понедельник, 22 ноября 2021 г.

Go для Java разработчиков: горутины и каналы

Горутины

Go позволяет запустить новый поток выполнения, горутину (goroutine), с помощью оператора go. Он запускает функцию в другой, недавно созданной горутине. Все горутины в одной программе используют одно и то же адресное пространство.

Горутины легкие и стоят немного больше, чем выделение места в стеке. Стеки начинаются с малого и увеличиваются за счет выделения и освобождения кучи по мере необходимости. Внутренне горутины действуют как сопрограммы (coroutines), которые мультиплексируются между несколькими потоками операционной системы.

go list.Sort() // Выполнить list.Sort параллельно.

В Go есть литералы функций, которые могут действовать как замыкания и эффективны в сочетании с оператором go.

// Publish печатает текст на стандартный вывод 
// по истечении заданного времени.
func Publish(text string, delay time.Duration) {
    go func() {
        time.Sleep(delay)
        fmt.Println(text)
    }() // Обратите внимание на круглые скобки. 
        // Мы должны вызвать функцию.
}

Переменные text и delay распределяются между окружающей функцией и литералом функции; они выживают, пока доступны.

Каналы

Канал предоставляет механизм для двух горутин для синхронизации выполнения и обмена данными, передавая значение определенного типа элемента. Оператор <- указывает направление канала, отправка или получение. Если направление не указано, канал двунаправленный.

chan Sushi     // может использоваться для отправки и получения значений типа Sushi
chan<- float64 // может использоваться только для отправки float64
<-chan int     // можно использовать только для получения целых чисел

Каналы являются ссылочным типом и назначаются с помощью make.

ic := make(chan int)       // небуферизованный канал целых чисел
wc := make(chan *Work, 10) // буферизованный канал указателей на Work

Чтобы отправить значение по каналу, используйте <- в качестве бинарного оператора. Чтобы получить значение по каналу, используйте его как унарный оператор.

ic <- 3      // Посылаем 3 по каналу.
work := <-wc // Получение указателя на Work из канала.

  • Если канал не буферизован, отправитель блокируется до тех пор, пока получатель не получит значение.
  • Если канал имеет буфер, отправитель блокируется только до тех пор, пока значение не будет скопировано в буфер; если буфер заполнен, это означает ожидание, пока какой-либо получатель не получит значение.
  • Получатели блокируются до тех пор, пока не появятся данные для приема.

Функция close записывает, что по каналу больше не будут отправляться значения:

  • После вызова close и после получения любых ранее отправленных значений операции приема вернут нулевое значение без блокировки.
  • Операция приема с несколькими значениями дополнительно возвращает указание, закрыт ли канал.

ch := make(chan string)
go func() {
    ch <- "Hello!"
    close(ch)
}()
fmt.Println(<-ch) // Печатает "Hello!".
fmt.Println(<-ch) // Распечатать нулевое значение "" без блокировки.
fmt.Println(<-ch) // Еще раз печатаем "".
v, ok := <-ch     // v - "", ok - false.

В следующем примере мы позволяем функции Publish возвращать канал, который используется для трансляции сообщения после публикации текста.

// Publish печатает текст на стандартный вывод по истечении заданного времени.
// Закрывает канал wait, когда текст был опубликован.
func Publish(text string, delay time.Duration) (wait <-chan struct{}) {
    ch := make(chan struct{})
    go func() {
        time.Sleep(delay)
        fmt.Println(text)
        close(ch)
    }()
    return ch
}

Вот как вы можете использовать эту Publish функцию.

wait := Publish("важные новости", 2 * time.Minute)
// Проделаем еще немного работы.
<-wait // блокируется, пока текст не будет опубликован

Оператор select

Оператор select - последний инструмент в наборе инструментов конкурентности Go. Он выбирает, какая из возможных коммуникаций будет продолжена:

  • если какой-либо из case может продолжаться, один из них выбирается случайным образом и выполняются соответствующие операторы;
  • в противном случае, если нет default case, инструкция блокируется до тех пор, пока не будет завершен один из case.

Вот простой пример, показывающий, как можно использовать оператор select для реализации генератора случайных чисел.

rand := make(chan int)
for { // Посылаем случайную последовательность битов в rand.
    select {
    case rand <- 0: // примечание: нет инструкции
    case rand <- 1:
    }
}

Более реалистично, вот как можно использовать оператор select для установки ограничения по времени для операции приема.

select {
case news := <-AFP:
    fmt.Println(news)
case <-time.After(time.Minute):
    fmt.Println(Тайм-аут: нет новостей в течение одной минуты.)
}

Функция time.After является частью стандартной библиотеки; она ожидает истечения указанного времени, а затем отправляет текущее время по возвращенному каналу.


Читайте также:


Купить gopher

четверг, 18 ноября 2021 г.

Go для Java разработчиков: ошибки, паника и восстановление

Ошибки

Там, где Java обычно использует исключения, Go имеет два разных механизма:

  • большинство функций возвращают ошибки;
  • только действительно невосстановимые условия, такие как индекс вне допустимого диапазона, создают исключения во время выполнения.

Многозначный возврат Go позволяет легко вернуть подробное сообщение об ошибке вместе с обычным возвращаемым значением. По соглашению такие сообщения имеют тип error, простой встроенный интерфейс.

type error interface {
    Error() string
}

Например, функция os.Open возвращает ненулевое значение ошибки, когда не удается открыть файл.

func Open(name string) (file *File, err error)

В следующем коде для открытия файла используется os.Open. Если возникает ошибка, он вызывает log.Fatal, чтобы распечатать сообщение об ошибке и остановиться.

f, err := os.Open("filename.ext")
if err != nil {
    log.Fatal(err)
}
// что-то делаем с открытым *File f

Интерфейс error требует только метода Error, но конкретные реализации ошибок часто имеют дополнительные методы, позволяющие вызывающим абонентам проверять подробности ошибки.

Паника и восстановление

Паника (panic) - это ошибка времени выполнения, которая раскручивает стек горутины, по пути запускает любые отложенные функции, а затем останавливает программу.

Паники похожи на исключения Java, но предназначены только для ошибок времени выполнения, таких как переход по нулевому указателю или попытка индексирования массива за пределами границ. Для обозначения таких событий, как конец файла, программы Go используют встроенный тип error, как описано выше.

Встроенную функцию recover можно использовать для восстановления контроля над паникующей горутиной и возобновления нормального выполнения:

  • вызов recover останавливает раскрутку и возвращает аргумент, переданный панике.

Поскольку единственный код, который выполняется при раскручивании, находится внутри отложенных (defer) функций, recover полезно только внутри отложенных функций. Если горутина не паникует, recover возвращает nil.


Читайте также:


среда, 17 ноября 2021 г.

Go для Java разработчиков: методы и интерфейсы

Метод выглядит как обычное определение функции, за исключением того, что у него есть получатель. Получатель аналогичен ссылке this в методе экземпляра Java.

type MyType struct { i int }

func (p *MyType) Get() int {
    return p.i
}

var pm = new(MyType)
var n = pm.Get()

Это объявляет метод Get, связанный с MyType. Получатель назван p в теле функции.

Методы объявлены для определенных типов. Если вы преобразовываете значение в другой тип, новое значение будет иметь методы нового типа, а не методы старого типа.

Вы можете определить методы для встроенного типа, объявив новый определенный тип, производный от него. Новый тип отличается от встроенного.

type MyInt int

func (p MyInt) Get() int {
    return int(p) // Требуется преобразование.
}

func f(i int) {}
var v MyInt

v = v * v // Операторы базового типа все еще применяются.
f(int(v)) // int(v) не имеет объявленных методов.
f(v)      // INVALID

Интерфейсы

Интерфейс Go похож на интерфейс Java, но любой тип, который предоставляет методы, названные в интерфейсе Go, можно рассматривать как реализацию этого интерфейса. Никакого явного объявления не требуется.

Предположим, что этот интерфейс определен:

type MyInterface interface {
    Get() int
    Set(i int)
}

Поскольку MyType уже имеет метод Get, мы можем заставить MyType удовлетворить интерфейс, добавив

func (p *MyType) Set(i int) {
    p.i = i
}

Теперь любая функция, которая принимает MyInterface в качестве параметра, будет принимать переменную типа *MyType.

func GetAndSet(x MyInterface) {}

func f1() {
    var p MyType
    GetAndSet(&p)
}

В терминах Java определение Set и Get для *MyType привело к тому, что *MyType автоматически реализует MyInterface. Тип может удовлетворять нескольким интерфейсам. Это форма неявной типизации (утиная типизация (англ. Duck typing)).

Когда я вижу птицу, которая ходит, как утка, плавает, как утка, и крякает, как утка, я называю эту птицу уткой. - Джеймс Уиткомб Райли

Встраивание (делегирование)

Встраивание типа как анонимного поля может использоваться для реализации формы подтипов.

type MySubType struct {
    MyType
    j int
}

func (p *MySubType) Get() int {
    p.j++
    return p.MyType.Get()
}

Это реализация MySubType как подтипа MyType.

func f2() {
    var p MySubType
    GetAndSet(&p)
}

Метод Set унаследован от MyType, поскольку методы, связанные с анонимным полем, повышаются до методов включающего типа.

В этом случае, поскольку MySubType имеет анонимное поле типа MyType, методы MyType также становятся методами MySubType. Метод Get был переопределен, а метод Set был унаследован.

Это не то же самое, что подкласс в Java, а форма делегирования. Когда вызывается метод анонимного поля, его получателем является поле, а не окружающая структура. Другими словами, методы анонимных полей не отправляются динамически. Если вам нужен эквивалент динамического поиска метода Java, используйте интерфейс.

func f3() {
    var v MyInterface

    v = new(MyType)
    v.Get() //  Вызов метода Get для *MyType.

    v = new(MySubType)
    v.Get() // Вызов метода Get для *MySubType.
}

Утверждения типа

Переменная, имеющая тип интерфейса, может быть преобразована в другой тип интерфейса с помощью утверждения типа. Это реализуется динамически во время выполнения. В отличие от Java, между двумя интерфейсами не требуется декларирования взаимосвязи.

type Printer interface {
    Print()
}

func f4(x MyInterface) {
    x.(Printer).Print() // утверждения типа для Printer
}

Преобразование в Printer полностью динамическое. Оно будет работать до тех пор, пока динамический тип x (фактический тип значения, хранящегося в x) определяет метод Print.


Читайте также:


четверг, 11 ноября 2021 г.

Go для Java разработчиков: срезы, создание значений

Срезы

Срез - это концептуально структура с тремя полями:

  • указатель на массив,
  • длина,
  • и емкость

Срезы поддерживают оператор [] для доступа к элементам базового массива.

  • Встроенная функция len возвращает длину среза.
  • Встроенная функция cap возвращает емкость.

Для данного массива или другого среза a новый срез создается через a[i:j].

  • Это создает новый срез, который ссылается на a, начинается с индекса i и заканчивается перед индексом j.
  • Имеет длину j - i.
  • Если i не указан, срез начинается с 0.
  • Если j опущен, срез заканчивается на len(a).

Новый срез ссылается на тот же массив, на который ссылается a. То есть изменения, внесенные в элементы с помощью нового среза, можно увидеть с помощью a.

Емкость нового среза - это просто емкость минус i. Емкость массива - это длина массива.

var s []int
var a [10]int

s = a[:] // сокращение от s = a[0:len(a)]

Если вы создаете значение типа [100]byte (массив из 100 байтов, возможно, буфер) и хотите передать его функции, не копируя его, объявите параметр функции как тип []byte и передайте срез массива. Срезы также можно создавать с помощью функции make, как описано ниже.

Срезы в сочетании со встроенной функцией append предлагают во многом те же функции, что и ArrayList в Java.

s0 := []int{1, 2}
s1 := append(s0, 3)     // добавляем один элемент
s2 := append(s1, 4, 5)  // добавляем несколько элементов
s3 := append(s2, s0...) // добавляем срез

Синтаксис среза также можно использовать со строкой. Он возвращает новую строку, значение которой является подстрокой исходной строки.

Создание значений

Значения карты и канала должны быть назначены с помощью встроенной функции make. Например, вызов

make(map[string]int)

возвращает вновь выделенное значение типа map[string]int.

В отличие от new, make возвращает фактический объект, а не адрес. Это согласуется с тем фактом, что карты и каналы являются ссылочными типами.

Для карт make принимает подсказку о емкости в качестве второго необязательного аргумента.

Для каналов есть необязательный второй аргумент, который устанавливает емкость буферизации канала; по умолчанию - 0 (без буферизации).

Функцию make также можно использовать для выделения среза. В этом случае он выделяет память для базового массива и возвращает срез, ссылающийся на него. Есть один обязательный аргумент - количество элементов в срезе. Второй необязательный аргумент - это емкость среза.

m := make([]int, 10, 20) // То же, что и new([20]int)[:10]


Читайте также:


среда, 3 ноября 2021 г.

Go для Java разработчиков: константы, структуры, указатели

Константы

В Go константы могут быть нетипизированными. Это относится к

  • числовым литералам,
  • выражениям, использующим только нетипизированные константы,
  • и объявлениям const, в которых тип не указан, а выражение инициализатора не типизировано.

Значение, полученное из нетипизированной константы, становится типизированным, когда оно используется в контексте, требующем типизированного значения. Это позволяет относительно свободно использовать константы, даже если в Go нет неявного преобразования типов.

var a uint
f(a + 1)   // Нетипизированная числовая константа 1 
           // становится типизированной как uint.
f(a + 1e3) // 1e3 также печатается как uint.

Язык не накладывает никаких ограничений на размер нетипизированной числовой константы. Предел применяется только тогда, когда константа используется там, где требуется тип.

const huge = 1 << 100
var n int = huge >> 98

Если тип отсутствует в объявлении переменной и соответствующее выражение оценивается как нетипизированная числовая константа, константа преобразуется в тип rune, int, float64 или complex128 соответственно, в зависимости от того, является ли значение константы символом, целым числом или числом с плавающей запятой, или комплексным числом.

c := 'å'    // rune (псевдоним для int32)
n := 1 + 2  // int
x := 2.7    // float64
z := 1 + 2i // complex128

В Go нет типов перечисления (enum). Вместо этого вы можете использовать специальное имя iota в одном объявлении const, чтобы получить серию возрастающих значений. Когда выражение инициализации опускается для константы, оно повторно использует предыдущее выражение.

const (
    red = iota // red == 0
    blue       // blue == 1
    green      // green == 2
)

Структуры

Структура соответствует классу в Java, но членами структуры не могут быть методы, только переменные. Указатель на структуру похож на ссылочную переменную в Java. В отличие от классов Java, структуры также могут быть определены как прямые значения. В обоих случаях вы используете . (точку) для доступа к членам структуры.

type MyStruct struct {
    s string
    n int64
}

var x MyStruct     // x инициализируется MyStruct{"", 0}.
var px *MyStruct   // px инициализируется nil.
px = new(MyStruct) // px указывает на новую структуру MyStruct{"", 0}.

x.s = "Foo"
px.s = "Bar"

В Go методы могут быть связаны с любым определяемым пользователем типом, а не только со структурами.

Указатели

Если у вас есть int, структура или массив, присваивание копирует содержимое объекта. Чтобы добиться эффекта ссылочных переменных Java, Go использует указатели.

Для любого типа T существует соответствующий тип указателя *T, обозначающий указатели на значения типа T.

Чтобы выделить память для переменной-указателя, используйте встроенную функцию new, которая принимает тип и возвращает указатель на выделенное хранилище. Выделенное пространство будет инициализировано нулевым значением для типа. Например, new(int) выделяет хранилище для нового int, инициализирует его значением 0 и возвращает его адрес, который имеет тип *int.

Код Java T p = new T(), где T - класс с двумя переменными экземпляра a и b типа int, соответствует

type T struct { a, b int }
var p *T = new(T)

или, что более идиоматично

p := new(T)

Объявление var v T, в котором объявляется переменная, содержащая значение типа T, не имеет эквивалента в Java. Значения также могут быть созданы и инициализированы с помощью составного литерала. Например:

v := T{1, 2}

эквивалентно

var v T
v.a = 1
v.b = 2

Для операнда x типа T оператор адреса &x дает адрес x, значение типа *T. Например:

p := &T{1, 2} // p имеет тип *T

Для операнда x типа указателя косвенное обращение к указателю *x обозначает значение, на которое указывает x. Косвенные указатели используются редко; Go, как и Java, может автоматически принимать адрес переменной.

p := new(T)
p.a = 1 // эквивалент (*p).a = 1


Читайте также:


вторник, 2 ноября 2021 г.

Go для Java разработчиков: синтаксис

Декларации

Синтаксис объявления обратный по сравнению с Java. Вы пишете имя, а затем тип. Объявления типов можно легко читать слева направо.

Go Приблизительный эквивалент Java
var v1 int int v1 = 0;
var v2 *int Integer v2 = null;
var v3 string String v3 = "";
var v4 [10]int int[] v4 = new int[10];
(Массивы - это значения в Go.)
var v5 []int int[] v5 = null;
var v6 *struct{ a int } class C { int a; }
C v6 = null;
var v7 map[string]int HashMap v7;
v7 = null;
var v8 func(a int) int interface F {
int f(int a);
}
F v8 = null;

Объявления обычно принимают форму ключевого слова, за которым следует имя объявляемого объекта. Ключевое слово может иметь одно из следующих значений: const, type, var или func. Вы также можете использовать ключевое слово, за которым следует ряд объявлений в круглых скобках.

var (
    n int
    x float64
)

При объявлении функции вы должны либо указать имя для каждого параметра, либо не указывать имя для любого параметра; нельзя опускать одни имена и указывать другие. Вы можете сгруппировать несколько имен одного типа.

func f(i, j, k int, s, t string)

Переменная может быть инициализирована при ее объявлении. Когда это будет сделано, указание типа переменной разрешено, но не обязательно. Если тип не указан, по умолчанию используется тип выражения инициализации.

var v9 = *v2

Если переменная не инициализирована явно, необходимо указать тип. В этом случае он будет неявно инициализирован нулевым значением типа (0, nil, "" и т. д.). В Go нет неинициализированных переменных.

Краткие объявления

Внутри функции краткий синтаксис объявления доступен с :=. Утверждение

v10 := v1

то же самое что и

var v10 = v1

Типы функций

В Go функции - это объекты первого класса. Тип функции Go обозначает набор всех функций с одинаковыми параметрами и типами результатов.

type binOp func(int, int) int

var op binOp
add := func(i, j int) int { return i + j }

op = add
n = op(100, 200)  // n = 100 + 200

Множественное присвоение

Go допускает несколько назначений. Выражения справа оцениваются перед присвоением любому из операндов слева.

i, j = j, i  // Меняем местами i и j.

Функции могут иметь несколько возвращаемых значений, обозначенных списком в круглых скобках. Возвращенные значения могут быть сохранены путем присвоения списку переменных.

func f() (i int, pj *int) { ... }
v1, v2 = f()

Пустой идентификатор

Пустой идентификатор, представленный символом подчеркивания, позволяет игнорировать значения, возвращаемые многозначным выражением:

v1, _ = f()  // Игнорировать второе значение, возвращаемое f().

Точка с запятой и форматирование

Вместо того чтобы беспокоиться о точках с запятой и форматировании, вы можете использовать программу gofmt для создания единого стандартного стиля Go. Хотя этот стиль поначалу может показаться странным, он ничем не уступает любому другому стилю, и знакомство с ним приведет к комфорту.

В коде Go на практике используется очень мало точек с запятой. Технически все операторы Go заканчиваются точкой с запятой. Однако Go неявно вставляет точку с запятой в конце непустой строки, если строка явно не является неполной. Следствием этого является то, что в некоторых случаях Go не допускает разрыв строки. Например, вы не можете писать

func g()
{            // INVALID: "{" должно быть в предыдущей строке.
}

Точка с запятой будет вставлена ​​после g(), что приведет к тому, что это будет объявление функции, а не определение функции. Точно так же вы не можете писать

if n == 0 {
}
else {       // INVALID: "else {" должно быть в предыдущей строке.
}

Точка с запятой будет вставлена ​​после символа } перед else, что вызовет синтаксическую ошибку.

Условные утверждения

Go не использует круглые скобки вокруг условия оператора if, выражений оператора for или значения оператора switch. С другой стороны, он требует фигурных скобок вокруг тела оператора if или for.

if a < b { f() }
if (a < b) { f() }          // Скобки не нужны.
if (a < b) f()              // INVALID
for i = 0; i < 10; i++ {}
for (i = 0; i < 10; i++) {} // INVALID

Кроме того, if и switch принимают необязательный оператор инициализации, который обычно используется для установки локальной переменной.

if err := file.Chmod(0664); err != nil {
    log.Print(err)
    return err
}

For утверждения

Go не имеет инструкции while и do-while. Оператор for может использоваться с одним условием, что делает его эквивалентным оператору while. Полное исключение условия приводит к бесконечному циклу.

Оператор for может содержать предложение диапазона для перебора строк, массивов, срезов, карт или каналов. Вместо того, чтобы писать

for i := 0; i < len(a); i++ { ... }

чтобы перебрать элементы a, мы также могли бы написать

for i, v := range a { ... }

Это присваивает i индексу, а v - значению последовательных элементов массива, среза или строки.

  • Для строк i - это индекс байта, а v - кодовая точка Unicode типа rune (rune - это псевдоним для int32).
  • Итерации по картам создают пары ключ-значение, тогда как каналы производят только одно значение итерации.

Break и continue

Подобно Java, Go разрешает break и continue для указания метки, но метка должна ссылаться на оператор for, switch или select.

Операторы switch

В операторе switch метки case не пропадают по умолчанию, но вы можете заставить их пропустить, завершив case оператором fallthrough.

switch n {
case 0: // пустое тело case
case 1:
    f() // f не вызывается, когда n == 0.
}

Но у case может быть несколько значений.

switch n {
case 0, 1:
    f() // f вызывается, если n == 0 || n == 1.
}

Значения в case могут быть любого типа, который поддерживает оператор сравнения равенства, например строки или указатели. Отсутствующее выражение switch эквивалентно выражению true.

switch {
case n < 0:
    f1()
case n == 0:
    f2()
default:
    f3()
}

Увеличение и уменьшение

++ и -- могут использоваться только как постфиксные операторы и только в операторах, но не в выражениях. Например, вы не можете написать n = i++.

Оператор defer

Оператор defer вызывает функцию, выполнение которой откладывается до момента возврата из окружающей функции.

  • Отложенная функция будет выполняться независимо от того, по какому пути будет возвращаться окружающая функция.
  • Однако параметры отложенной функции вычисляются и сохраняются для будущего использования уже при выполнении оператора defer.

f, err := os.Open("filename")
defer f.Close() // f будет закрыт, когда эта функция вернется.


Читайте также:


четверг, 28 октября 2021 г.

Go для Java разработчиков: основные отличия Go

Объектно-ориентированное программирование

  • В Go нет классов с конструкторами. Вместо методов экземпляра, иерархии наследования классов и поиска динамических методов Go предоставляет структуры и интерфейсы.
  • Go позволяет использовать методы любого типа; упаковка не требуется. Получатель метода, который соответствует this в Java, может быть прямым значением или указателем.
  • Go предоставляет два уровня доступа, аналогичные public и package-private в Java. Объявления верхнего уровня являются общедоступными, если их имена начинаются с заглавной буквы, в противном случае они являются частными для пакета.

Функциональное программирование

  • Функции в Go - это объекты первого класса. Значения функций могут использоваться и передаваться так же, как и другие значения, а функциональные литералы могут относиться к переменным, определенным во включающей функции.

Указатели и ссылки

  • Go предлагает указатели на значения всех типов, а не только на объекты и массивы. Для любого типа T существует соответствующий тип указателя *T, обозначающий указатели на значения типа T.
  • Go использует nil для недопустимых указателей, где Java использует null.
  • Массивы в Go - это значения. Когда массив используется в качестве параметра функции, функция получает копию массива, а не указатель на нее. Однако на практике функции часто используют срезы для параметров; срезы - это ссылки на базовые массивы.
  • Определенные типы (карты, срезы и каналы) передаются по ссылке, а не по значению. То есть передача карты функции не копирует карту; если функция изменяет карту, вызывающий абонент увидит это изменение. В терминах Java это можно рассматривать как ссылку на карту.

Встроенные типы

  • Строки предоставляются языком; строка ведет себя как срез байтов, но является неизменной.
  • Хэш-таблицы предоставляются языком. Их называют картами.

Обработка ошибок

  • Вместо исключений Go использует ошибки для обозначения событий, таких как конец файла, и паники во время выполнения для ошибок времени выполнения, таких как попытка индексирования массива за пределами границ.

Конкурентность

  • Отдельные потоки выполнения, горутины и каналы связи между ними, каналы, предоставляются языком.

Отсутствующие функции

  • Go не поддерживает неявное преобразование типов. Операции, сочетающие разные типы, требуют явного преобразования. Вместо этого Go предлагает нетипизированные числовые константы без ограничений.
  • Go не поддерживает перегрузку функций. Функции и методы в одной области должны иметь уникальные имена.
  • Go имеет несколько встроенных универсальных типов данных, таких как срезы и карты, а также универсальные функции, такие как append и copy. Однако не существует механизма для написания ваших собственных универсальных функций.

Читайте также:


среда, 27 октября 2021 г.

Go для Java разработчиков: Hello world пример

В постах Go для Java разработчиков будут представлены основные отличия Go для быстрого освоения Java разработчиками.

Начнем с небольшого, но полного примера. Он показывает, как реализовать и использовать простой абстрактный тип данных в Go. В данном случае это стек.

// Пакет collection реализует стек строк.
package collection

// Нулевое значение для Stack - это пустой стек, 
// готовый к использованию.
type Stack struct {
    data []string
}

// Push добавляет x на вершину стека.
func (s *Stack) Push(x string) {
    s.data = append(s.data, x)
}

// Pop удаляет и возвращает верхний элемент стека.
// Вызов Pop для пустого стека - ошибка времени выполнения.
func (s *Stack) Pop() string {
    n := len(s.data) - 1
    res := s.data[n]
    s.data[n] = "" // чтобы избежать утечки памяти
    s.data = s.data[:n]
    return res
}

// Size возвращает количество элементов в стеке.
func (s *Stack) Size() int {
    return len(s.data)
}

  • Комментарии, которые появляются непосредственно перед объявлениями верхнего уровня, являются комментариями документации. Они написаны обычным текстом.
  • Для объявлений вы пишете имя, а затем тип.
  • Структура (struct) соответствует классу в Java, но членами структуры не могут быть методы, только переменные.
  • Фрагмент кода (s *Stack) объявляет приемник метода s, соответствующий Java this.
  • Оператор := объявляет и инициализирует переменную. Его тип выводится из выражения инициализации.

Вот программа Hello world, которая показывает, как использовать абстрактный тип данных collection.Stack.

package collection_test

import (
    "fmt"
    "go-for-java-programmers/collection"
)

func ExampleStack() {
    var s collection.Stack
    s.Push("world!")
    s.Push("Hello, ")
    for s.Size() > 0 {
        fmt.Print(s.Pop())
    }
    fmt.Println()
    // Вывод: Hello, world!
}

  • Тестовый пакет collection_test находится в том же каталоге, что и пакет collection.
  • Первое объявление импорта содержит путь "fmt" к стандартному пакету; второй указывает, что мы будем использовать пакет из каталога "go-for-java-programmers/collection".
  • Доступ к пакетам в исходном файле осуществляется по коротким именам fmt и collection соответственно.

Примечание: идиоматический способ реализовать стек в Go - это использовать срез напрямую. Смотрите этот пост, о том как реализовать стек (LIFO).


Читайте также:


понедельник, 18 октября 2021 г.

Golang tips&tricks: таймауты и крайние сроки

Чтобы отказаться от синхронных вызовов, которые выполняются слишком долго, используйте оператор select с time.After:

import "time"

c := make(chan error, 1)
go func() { c <- client.Call("Service.Method", args, &reply) } ()
select {
  case err := <-c:
    // использование err и reply
  case <-time.After(timeoutNanoseconds):
    // истечение времени вызова
}

Обратите внимание, что канал c имеет размер буфера 1. Если бы это был небуферизованный канал и метод client.Call занимал больше времени timeoutNanoseconds, передача канала заблокировалась бы навсегда, и горутина никогда не была бы уничтожена.


Читайте также:


четверг, 7 октября 2021 г.

Пустой интерфейс в Golang

Пустой интерфейс может использоваться для хранения любых данных, и он может быть полезным параметром, поскольку может работать с любым типом. Чтобы понять, как работает пустой интерфейс и как он может содержать любой тип, мы должны сначала понять концепцию, лежащую в его основе.

Пустой интерфейс

Вот хорошее определение пустого интерфейса:

Интерфейс - это два компонента: это набор методов и тип.

Тип interface{} - это интерфейс, не имеющий методов. Поскольку в Go нет ключевого слова implements (реализует), все типы реализуют по крайней мере ноль методов, и удовлетворение интерфейса выполняется автоматически, все типы удовлетворяют пустой интерфейс. Следовательно, метод с пустым интерфейсом в качестве аргумента может принимать любой тип. Go перейдет к преобразованию в тип интерфейса, который будет выполнять эту функцию.

Расс Кокс, один из ведущих разработчиков Go, написал статью о внутреннем представлении интерфейсов и объяснил, что интерфейс состоит из двух слов:

  • указатель на информацию о сохраненном типе
  • указатель на связанные данные

Эта было в 2009 году, когда среда выполнения была написана на C.

Среда выполнения теперь написана на Go, но представление осталось прежним. Мы можем убедиться в этом, распечатав пустой интерфейс:

package main

func main() {
    var myVar int32 = 1
    printInterface(myVar)
}

//go:noinline
func printInterface(val interface{}) {
    println(val)
}

Вывод:

(0x459dc0,0xc00003476c)

Оба адреса представляют собой два указателя на тип информации и значение.

Базовая структура

Базовое представление пустого интерфейса задокументировано в пакете reflect:

type emptyInterface struct {
   typ  *rtype            // слово 1 с описанием типа
   word unsafe.Pointer    // слово 2 со значением
}

Как объяснялось ранее, мы ясно видим, что пустой интерфейс имеет слово описания типа, за которым следует слово, содержащее данные.

Структура rtype содержит основу описания типа:

type rtype struct {
	size       uintptr
	ptrdata    uintptr // количество байтов в типе, которое может содержать указатели
	hash       uint32  // хэш типа; избегает вычислений в хэш-таблицах
	tflag      tflag   // флаги дополнительной информации типа
	align      uint8   // выравнивание переменной с этим типом
	fieldAlign uint8   // выравнивание поля структуры с этим типом
	kind       uint8   // перечисление для C
	// функция для сравнения объектов этого типа
	// (ptr to object A, ptr to object B) -> ==?
	equal     func(unsafe.Pointer, unsafe.Pointer) bool
	gcdata    *byte   // данные сборки мусора
	str       nameOff // строковая форма
	ptrToThis typeOff //  тип для указателя на этот тип, может быть нулевым
}

Среди этих полей некоторые довольно простые и хорошо известные:

  • size - это размер в байтах
  • kind содержит тип: int8, int16, bool и т. д.
  • align - это выравнивание переменной с этим типом

В зависимости от типа, встроенного в пустой интерфейс, мы можем сопоставить экспортируемые поля или перечислить методы:

// structType представляет struct тип.
type structType struct {
	rtype
	pkgPath name
	fields  []structField
}

В типе структуры есть еще два поля, включая список полей структуры. Это ясно показывает, что преобразование встроенного типа (rtype) в пустой интерфейс приведет к плоскому преобразованию, при котором описание поля и его значение будут сохранены в памяти.

Давайте теперь посмотрим, какие виды конверсии действительно возможны из пустого интерфейса.

Конверсии

Давайте попробуем простую программу, которая использует пустой интерфейс с неправильным преобразованием:

package main

func main() {
    var myVar int32 = 1
    printInterface(myVar)
}

//go:noinline
func printInterface(val interface{}) {
    n := val.(int64)
    println(n)
}

Хотя преобразование из int32 в int64 допустимо, программа запаникует:

panic: interface conversion: interface {} is int32, not int64

goroutine 1 [running]:
main.printInterface(0xe49f20, 0xc00002ff74)
	/home/myuser/main.go:10 +0x85
main.main()
	/home/myuser/main.go:5 +0x46

Давайте сгенерируем asm-код, чтобы увидеть, какая проверка выполняется Go:

...
0x0024 00036 (/home/myuser/main.go:10)  LEAQ    type.int64(SB), AX // Шаг 1
0x002b 00043 (/home/myuser/main.go:10)  MOVQ    "".val+48(SP), CX
0x0030 00048 (/home/myuser/main.go:10)  CMPQ    AX, CX
0x0033 00051 (/home/myuser/main.go:10)  JNE     105                // Шаг 2
0x0035 00053 (/home/myuser/main.go:10)  MOVQ    "".val+56(SP), AX
0x003a 00058 (/home/myuser/main.go:10)  MOVQ    (AX), AX
0x003d 00061 (/home/myuser/main.go:10)  MOVQ    AX, "".n+24(SP)
0x0042 00066 (/home/myuser/main.go:11)  PCDATA  $1, $1
0x0042 00066 (/home/myuser/main.go:11)  CALL    runtime.printlock(SB)
0x0047 00071 (/home/myuser/main.go:11)  MOVQ    "".n+24(SP), AX
0x004c 00076 (/home/myuser/main.go:11)  MOVQ    AX, (SP)
0x0050 00080 (/home/myuser/main.go:11)  CALL    runtime.printint(SB)
0x0055 00085 (/home/myuser/main.go:11)  CALL    runtime.printnl(SB)
0x005a 00090 (/home/myuser/main.go:11)  CALL    runtime.printunlock(SB)
0x005f 00095 (/home/myuser/main.go:12)  MOVQ    32(SP), BP
0x0064 00100 (/home/myuser/main.go:12)  ADDQ    $40, SP
0x0068 00104 (/home/myuser/main.go:12)  RET
0x0069 00105 (/home/myuser/main.go:10)  MOVQ    CX, (SP)                
0x006d 00109 (/home/myuser/main.go:10)  MOVQ    AX, 8(SP)
0x0072 00114 (/home/myuser/main.go:10)  LEAQ    type.interface {}(SB), AX
0x0079 00121 (/home/myuser/main.go:10)  MOVQ    AX, 16(SP)
0x007e 00126 (/home/myuser/main.go:10)  NOP
0x0080 00128 (/home/myuser/main.go:10)  CALL    runtime.panicdottypeE(SB) // Шаг 3
0x0085 00133 (/home/myuser/main.go:10)  XCHGL   AX, AX                    // Шаг 4
0x0086 00134 (/home/myuser/main.go:10)  NOP
0x0086 00134 (/home/myuser/main.go:9)   PCDATA  $1, $-1
0x0086 00134 (/home/myuser/main.go:9)   PCDATA  $0, $-2
0x0086 00134 (/home/myuser/main.go:9)   CALL    runtime.morestack_noctxt(SB) 
0x008b 00139 (/home/myuser/main.go:9)   PCDATA  $0, $-1
0x008b 00139 (/home/myuser/main.go:9)   JMP     0
...

Вот несколько шагов:

  • Шаг 1: сравнить (инструкция CMPQ) тип int64 (загруженный инструкцией LEAQ, Load Effective Address) с внутренним типом пустого интерфейса (инструкция MOVQ, которая считывает память со смещением 48 байтов из сегмента памяти пустого интерфейса)
  • Шаг 2: инструкция JNE "Перейти, если не равно" перейдет к сгенерированным инструкциям, которые будут обрабатывать ошибку на шаге 3.
  • Шаг 3: код запаникует и выдаст сообщение об ошибке, которое мы видели ранее
  • Шаг 4: это конец инструкций по ошибке. На эту конкретную инструкцию ссылается сообщение об ошибке, которое показывает инструкцию: main.go: 10 + 0x85

Любое преобразование из внутреннего типа пустого интерфейса должно выполняться после преобразования исходного типа. Это преобразование в пустой интерфейс, а затем обратно в исходный тип, требует затрат для вашей программы. Давайте проведем несколько тестов, чтобы получить общее представление об этом.

Производительность

Вот два теста (Golang 1.16.2). Один с копией структуры, а другой с использованием пустого интерфейса:

package maintest

import (
    "testing"
)

var instance StructWithFields

type StructWithFields struct {
    field1 int
    field2 string
    field3 float32
    field4 float64
    field5 int32
    field6 bool
    field7 uint64
    field8 *string
    field9 uint16
}

//go:noinline
func emptyInterface(i interface {}) {
    str := i.(StructWithFields)
    instance = str
}

//go:noinline
func withType(str StructWithFields) {
    instance = str
}

func BenchmarkWithType(b *testing.B) {
    str := StructWithFields{field1: 1, field2: "string", field8: new(string)}
    for i := 0; i < b.N; i++ {
        withType(str)
    }
}

func BenchmarkWithEmptyInterface(b *testing.B) {
    str := StructWithFields{field1: 1, field2: "string", field8: new(string)}
    for i := 0; i < b.N; i++ {
        emptyInterface(str)
    }
}

Вот результаты:

BenchmarkWithType-4             	74843296  16.75 ns/op
BenchmarkWithEmptyInterface-4   	48790801  33.27 ns/op

Для двойного преобразования типа в пустой интерфейс, а затем обратно в тип требуется на 16 наносекунд больше, чем копирование структуры.

Хорошим решением было бы использовать указатель и преобразовать обратно в тот же указатель на структуру.

package maintest

import (
    "testing"
)

var instance *StructWithFields

type StructWithFields struct {
    field1 int
    field2 string
    field3 float32
    field4 float64
    field5 int32
    field6 bool
    field7 uint64
    field8 *string
    field9 uint16
}

//go:noinline
func emptyInterface(i interface {}) {
    str := i.(*StructWithFields)
    instance = str
}

//go:noinline
func withType(str *StructWithFields) {
    instance = str
}

func BenchmarkWithType(b *testing.B) {
    str := StructWithFields{field1: 1, field2: "string", field8: new(string)}
    for i := 0; i < b.N; i++ {
        withType(&str)
    }
}

func BenchmarkWithEmptyInterface(b *testing.B) {
    str := StructWithFields{field1: 1, field2: "string", field8: new(string)}
    for i := 0; i < b.N; i++ {
        emptyInterface(&str)
    }
}

Результаты теперь совсем другие:

BenchmarkWithType-4             	187950846  6.374 ns/op
BenchmarkWithEmptyInterface-4   	150776062  8.141 ns/op

Что касается базового типа, такого как int или string, производительность немного отличается:

package maintest

import (
    "testing"
)

var intVar int
var stringVar string

//go:noinline
func emptyInterfaceInt(i interface {}) {
    str := i.(int)
    intVar = str
}

//go:noinline
func withTypeInt(str int) {
    intVar = str
}

//go:noinline
func emptyInterfaceString(i interface {}) {
    str := i.(string)
    stringVar = str
}

//go:noinline
func withTypeString(str string) {
    stringVar = str
}

func BenchmarkWithTypeInt(b *testing.B) {
    str := 123
    for i := 0; i < b.N; i++ {
        withTypeInt(str)
    }
}

func BenchmarkWithEmptyInterfaceInt(b *testing.B) {
    str := 123
    for i := 0; i < b.N; i++ {
        emptyInterfaceInt(str)
    }
}

func BenchmarkWithTypeString(b *testing.B) {
    str := "123"
    for i := 0; i < b.N; i++ {
        withTypeString(str)
    }
}

func BenchmarkWithEmptyInterfaceString(b *testing.B) {
    str := "123"
    for i := 0; i < b.N; i++ {
        emptyInterfaceString(str)
    }
}

BenchmarkWithTypeInt-4                	228122656  5.319 ns/op
BenchmarkWithEmptyInterfaceInt-4      	144611329  8.028 ns/op
BenchmarkWithTypeString-4             	158939217  7.508 ns/op
BenchmarkWithEmptyInterfaceString-4   	100000000  10.10 ns/op

Пустой интерфейс в большинстве случаев может быть полезен в ваших приложениях, при правильном использовании и с осторожностью, учитывая его влияние на производительность.


Читайте также:


среда, 6 октября 2021 г.

Ограничение использования ресурсов для программы в Golang

Чтобы ограничить использование программой ресурса, такого как память, пусть горутины синхронизируют использование этого ресурса с помощью буферизованного канала (т. е. используют канал как семафор):

const (
    AvailableMemory         = 10 << 20 // 10 MB
    AverageMemoryPerRequest = 10 << 10 // 10 KB
    MaxOutstanding          = AvailableMemory / AverageMemoryPerRequest
)

var sem = make(chan int, MaxOutstanding)

func Serve(queue chan *Request) {
    for {
        sem <- 1 // Блокировать до тех пор, 
                 // пока не появится возможность обработать запрос.
        req := <-queue
        go handle(req) // Не дожидаемся завершения handle.
    }
}

func handle(r *Request) {
    process(r) // Может занять много времени 
               // и потреблять много памяти или ЦП
    <-sem      // Готово; разрешить запуск следующего запроса.
}


Читайте также:


среда, 29 сентября 2021 г.

Пакет net/url в Golang

Пакет url анализирует URL-адреса и реализует экранирование запросов.

Функция PathEscape

func PathEscape(s string) string

PathEscape экранирует строку, чтобы ее можно было безопасно разместить внутри сегмента пути URL, заменяя при необходимости специальные символы (включая /) последовательностями %XX.

Пример использования функции PathEscape

package main

import (
    "fmt"
    "net/url"
)

func main() {
    space := url.PathEscape("one two")
    fmt.Println(space)
    
    percent := url.PathEscape("10%")
    fmt.Println(percent)
    
    symbols := url.PathEscape(" ?&=#+%!<>#\"{}|\\^[]`☺\t:/@$'()*,;")
    fmt.Println(symbols)
}

Вывод:

one%20two
10%25
%20%3F&=%23+%25%21%3C%3E%23%22%7B%7D%7C%5C%5E%5B%5D%60%E2%98%BA%09:%2F@$%27%28%29%2A%2C%3B

Пример в песочнице https://play.golang.org/p/LNmUJmU8uaq

Функция PathUnescape

func PathUnescape(s string) (string, error)

PathUnescape выполняет обратное преобразование PathEscape, преобразуя каждую 3-байтовую закодированную подстроку формы "%AB" в шестнадцатеричный байт 0xAB. Он возвращает ошибку, если за любым % не следуют две шестнадцатеричные цифры.

PathUnescape идентичен QueryUnescape, за исключением того, что он не отменяет экранирование '+' на ' ' (пробел).

Пример использования функции PathUnescape

package main

import (
	"fmt"
	"net/url"
)

func main() {
	space, err := url.PathUnescape("one%20two")
	if err != nil {
		fmt.Println(err.Error())
	}
	fmt.Println(space)
	
	percent, err := url.PathUnescape("10%25")
	if err != nil {
		fmt.Println(err.Error())
	}
	fmt.Println(percent)
	
	symbols, err := url.PathUnescape("%20%3F&=%23+%25%21%3C%3E%23%22%7B%7D%7C%5C%5E%5B%5D%60%E2%98%BA%09:%2F@$%27%28%29%2A%2C%3B")
	if err != nil {
		fmt.Println(err.Error())
	}
	fmt.Println(symbols)
}

Вывод:

one two
10%
 ?&=#+%!<>#"{}|\^[]`☺	:/@$'()*,;

Пример в песочнице https://play.golang.org/p/RevOg4UxSG7

Функция QueryEscape

func QueryEscape(s string) string

QueryEscape экранирует строку, чтобы ее можно было безопасно поместить в URL-запрос.

Пример использования функции QueryEscape

package main

import (
    "fmt"
    "net/url"
)

func main() {
    space := url.QueryEscape("one two")
    fmt.Println(space)
    
    percent := url.QueryEscape("10%")
    fmt.Println(percent)
    
    symbols := url.QueryEscape(" ?&=#+%!<>#\"{}|\\^[]`☺\t:/@$'()*,;")
    fmt.Println(symbols)
}

Вывод:

one+two
10%25
+%3F%26%3D%23%2B%25%21%3C%3E%23%22%7B%7D%7C%5C%5E%5B%5D%60%E2%98%BA%09%3A%2F%40%24%27%28%29%2A%2C%3B

Пример в песочнице https://play.golang.org/p/hfKAuFsV00i

Функция QueryUnescape

func QueryUnescape(s string) (string, error)

QueryUnescape выполняет обратное преобразование QueryEscape, преобразуя каждую 3-байтовую закодированную подстроку формы "%AB" в шестнадцатеричный байт 0xAB. Он возвращает ошибку, если за любым % не следуют две шестнадцатеричные цифры.

Пример использования функции QueryUnescape

package main

import (
    "fmt"
    "net/url"
)

func main() {
    space, err := url.QueryUnescape("one+two")
    if err != nil {
        fmt.Println(err.Error())
    }
    fmt.Println(space)
    
    percent, err := url.QueryUnescape("10%25")
    if err != nil {
        fmt.Println(err.Error())
    }
    fmt.Println(percent)
    
    symbols, err := url.QueryUnescape("+%3F%26%3D%23%2B%25%21%3C%3E%23%22%7B%7D%7C%5C%5E%5B%5D%60%E2%98%BA%09%3A%2F%40%24%27%28%29%2A%2C%3B")
    if err != nil {
        fmt.Println(err.Error())
    }
    fmt.Println(symbols)
}

Вывод:

one two
10%
 ?&=#+%!<>#"{}|\^[]`☺	:/@$'()*,;

Пример в песочнице https://play.golang.org/p/cIDrkclH5Qa

Тип URL

type URL struct {
    Scheme      string
    Opaque      string    // закодированные непрозрачные данные
    User        *Userinfo // username и password информация
    Host        string    // host или host:port
    Path        string    // путь (в относительных путях может отсутствовать начальная косая черта)
    RawPath     string    // закодированный путь (EscapedPath)
    ForceQuery  bool      // добавить запрос ('?') даже если RawQuery пуст
    RawQuery    string    // закодированные значения запроса, без '?'
    Fragment    string    // фрагмент для ссылок, без '#'
    RawFragment string    // закодированный фрагмент (EscapedFragment)
}

URL представляет собой проанализированный URL-адрес (технически ссылка на URI).

Общая форма представленна как:

[scheme:][//[userinfo@]host][/]path[?query][#fragment]

URL-адреса, которые не начинаются с косой черты после схемы, интерпретируются как:

scheme:opaque[?query][#fragment]

Обратите внимание, что поле Path хранится в декодированной форме: /%47%6f%2f становится /Go/. Как следствие, невозможно определить, какие косые черты в пути были косыми чертами в необработанном URL, а какие были %2f. Это различие редко бывает важным, но когда это так, код должен использовать RawPath, необязательное поле, которое устанавливается только в том случае, если кодировка по умолчанию отличается от Path.

Метод URL String использует метод EscapedPath для получения пути.

Пример использования типа URL

package main

import (
    "fmt"
    "log"
    "net/url"
)

func main() {
    u, err := url.Parse("http://bing.com/search?q=dotnet")
    if err != nil {
        log.Fatal(err)
    }
    u.Scheme = "https"
    u.Host = "google.com"
    q := u.Query()
    q.Set("q", "golang")
    u.RawQuery = q.Encode()
    fmt.Println(u)
}

Вывод

https://google.com/search?q=golang

Пример использования типа URL

package main

import (
    "fmt"
    "log"
    "net/url"
)

func main() {
    // Parse + String сохраняют первоначальное кодирование.
    u, err := url.Parse("https://example.com/foo%2fbar")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(u.Path)
    fmt.Println(u.RawPath)
    fmt.Println(u.String())
}

Вывод

/foo/bar
/foo%2fbar
https://example.com/foo%2fbar

Функция Parse

func Parse(rawURL string) (*URL, error)

Parse анализирует необработанный URL-адрес в структуру URL.

URL-адрес может быть относительным (путь без хоста) или абсолютным (начиная со схемы). Попытка проанализировать имя хоста и путь без схемы недопустима, но может не обязательно возвращать ошибку из-за неоднозначности синтаксического анализа.

Пример использования функции Parse

package main

import (
    "fmt"
    "log"
    "net/url"
)

func main() {
    u, err := url.Parse("http://bing.com/search?q=dotnet")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Scheme: ", u.Scheme)
    fmt.Println("Host: ", u.Host)
    fmt.Println("Path: ", u.Path)
    fmt.Println("RawQuery: ", u.RawQuery)
}

Вывод

Scheme:  http
Host:  bing.com
Path:  /search
RawQuery:  q=dotnet

Пример в песочнице https://play.golang.org/p/o5gLj5_fteS

Функция ParseRequestURI

func ParseRequestURI(rawURL string) (*URL, error)

ParseRequestURI анализирует необработанный URL-адрес в структуру URL. Предполагается, что URL-адрес был получен в HTTP-запросе, поэтому URL-адрес интерпретируется только как абсолютный URI или абсолютный путь. Предполагается, что строковый url не имеет суффикса #fragment. (Веб-браузеры удаляют #fragment перед отправкой URL-адреса на веб-сервер.)

Метод EscapedFragment

func (u *URL) EscapedFragment() string

EscapedFragment возвращает экранированную форму u.Fragment. В общем, существует несколько возможных экранированных форм любого фрагмента. EscapedFragment возвращает u.RawFragment, если это допустимое экранирование u.Fragment. В противном случае EscapedFragment игнорирует u.RawFragment и самостоятельно вычисляет экранированную форму. Метод String использует EscapedFragment для построения своего результата. В общем, код должен вызывать EscapedFragment вместо непосредственного чтения u.RawFragment.

Пример использования метода EscapedFragment

package main

import (
    "fmt"
    "log"
    "net/url"
)

func main() {
    u, err := url.Parse("http://example.com/#x/y%2Fz")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Fragment:", u.Fragment)
    fmt.Println("RawFragment:", u.RawFragment)
    fmt.Println("EscapedFragment:", u.EscapedFragment())
}

Вывод

Fragment: x/y/z
RawFragment: x/y%2Fz
EscapedFragment: x/y%2Fz

Пример в песочнице https://play.golang.org/p/Q-I-8bfoXFO

Метод EscapedPath

func (u *URL) EscapedPath() string

EscapedPath возвращает экранированную форму u.Path. Как правило, у любого пути есть несколько возможных экранированных форм. EscapedPath возвращает u.RawPath, если это действительное экранирование u.Path. В противном случае EscapedPath игнорирует u.RawPath и вычисляет экранированную форму самостоятельно. Методы String и RequestURI используют EscapedPath для построения своих результатов. В общем, код должен вызывать EscapedPath вместо непосредственного чтения u.RawPath.

Пример использования метода EscapedPath

package main

import (
    "fmt"
    "log"
    "net/url"
)

func main() {
    u, err := url.Parse("http://example.com/x/y%2Fz")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Path:", u.Path)
    fmt.Println("RawPath:", u.RawPath)
    fmt.Println("EscapedPath:", u.EscapedPath())
}

Вывод

Path: /x/y/z
RawPath: /x/y%2Fz
EscapedPath: /x/y%2Fz

Пример в песочнице https://play.golang.org/p/xvc45jZkqq6

Метод Hostname

func (u *URL) Hostname() string

Hostname возвращает u.Host, удаляя любой допустимый номер порта, если он присутствует.

Если результат заключен в квадратные скобки, как буквальные адреса IPv6, квадратные скобки удаляются из результата.

Пример использования метода Hostname

package main

import (
    "fmt"
    "log"
    "net/url"
)

func main() {
    u, err := url.Parse("https://example.org:8000/path")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(u.Hostname())
    u, err = url.Parse("https://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:17000")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(u.Hostname())
}

Вывод

example.org
2001:0db8:85a3:0000:0000:8a2e:0370:7334

Пример в песочнице https://play.golang.org/p/0MV8vkqssmO

Метод IsAbs

func (u *URL) IsAbs() bool

IsAbs сообщает, является ли URL абсолютным. Абсолютное означает, что у него непустая схема.

Пример использования метода IsAbs

package main

import (
    "fmt"
    "net/url"
)

func main() {
    u := url.URL{Host: "example.com", Path: "foo"}
    fmt.Println(u.IsAbs())
    u.Scheme = "http"
    fmt.Println(u.IsAbs())
}

Вывод

false
true

Пример в песочнице https://play.golang.org/p/UfOBOdHopyF

Метод MarshalBinary

func (u *URL) MarshalBinary() (text []byte, err error)

Преобразует URL в срез байтов, выдает ошибку если она возникает при преобразовании.

Пример использования метода MarshalBinary

package main

import (
    "fmt"
    "log"
    "net/url"
)

func main() {
    u, _ := url.Parse("https://example.org")
    b, err := u.MarshalBinary()
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("%s\n", b)
}

Вывод

https://example.org

Пример в песочнице https://play.golang.org/p/L2uSp9WIN1Y

Метод Parse

func (u *URL) Parse(ref string) (*URL, error)

Parse анализирует URL-адрес в контексте получателя. Предоставленный URL-адрес может быть относительным или абсолютным. Parse возвращает nil, ошибку при сбое синтаксического анализа, в противном случае его возвращаемое значение такое же, как ResolveReference.

Пример использования метода Parse

package main

import (
    "fmt"
    "log"
    "net/url"
)

func main() {
    u, err := url.Parse("https://example.org")
    if err != nil {
        log.Fatal(err)
    }
    rel, err := u.Parse("/foo")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(rel)
    _, err = u.Parse(":foo")
    if _, ok := err.(*url.Error); !ok {
        log.Fatal(err)
    }
}

Вывод

https://example.org/foo

Пример в песочнице https://play.golang.org/p/lyPqaigN1pw

Метод Port

func (u *URL) Port() string

Port возвращает часть порта u.Host без начального двоеточия.

Если u.Host не содержит допустимого числового порта, Port возвращает пустую строку.

Пример использования метода Port

package main

import (
    "fmt"
    "log"
    "net/url"
)

func main() {
    u, err := url.Parse("https://example.org")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(u.Port())
    u, err = url.Parse("https://example.org:8080")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(u.Port())
}

Вывод

8080

Пример в песочнице https://play.golang.org/p/xcP1_ozPxfG

Метод Query

func (u *URL) Query() Values

Query анализирует RawQuery и возвращает соответствующие значения. Он молча отбрасывает искаженные пары значений. Для проверки ошибок используйте ParseQuery.

Пример использования метода Query

package main

import (
    "fmt"
    "log"
    "net/url"
)

func main() {
    u, err := url.Parse("https://example.org/?a=1&a=2&b=&=3&&&&")
    if err != nil {
        log.Fatal(err)
    }
    q := u.Query()
    fmt.Println(q["a"])
    fmt.Println(q.Get("b"))
    fmt.Println(q.Get(""))
}

Вывод

[1 2]

3

Пример в песочнице https://play.golang.org/p/MJrTerqSaqd

Метод Redacted

func (u *URL) Redacted() string

Redacted аналогично String, но заменяет любой пароль на "xxxxx". Удаляется только пароль в u.URL.

Пример использования метода Redacted

package main

import (
    "fmt"
    "net/url"
)

func main() {
    u := &url.URL{
        Scheme: "https",
        User:   url.UserPassword("user", "password"),
        Host:   "example.com",
        Path:   "foo/bar",
    }
    fmt.Println(u.Redacted())
    u.User = url.UserPassword("me", "newerPassword")
    fmt.Println(u.Redacted())
}

Вывод

https://user:xxxxx@example.com/foo/bar
https://me:xxxxx@example.com/foo/bar

Пример в песочнице https://play.golang.org/p/FPsxk_4bbCV

Метод RequestURI

func (u *URL) RequestURI() string

RequestURI возвращает закодированный path?query или opaque?query строку, которая будет использоваться в HTTP-запросе для u.

Пример использования метода RequestURI

package main

import (
    "fmt"
    "log"
    "net/url"
)

func main() {
    u, err := url.Parse("https://example.org/path?foo=bar")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(u.RequestURI())
}

Вывод

/path?foo=bar

Пример в песочнице https://play.golang.org/p/QK-lScEgjou

Метод ResolveReference

func (u *URL) ResolveReference(ref *URL) *URL

ResolveReference разрешает ссылку URI на абсолютный URI из абсолютного базового URI u, согласно RFC 3986. Ссылка URI может быть относительной или абсолютной. ResolveReference всегда возвращает новый экземпляр URL, даже если возвращенный URL идентичен базовому или ref. Если ref является абсолютным URL, ResolveReference игнорирует base и возвращает копию ref.

Пример использования метода ResolveReference

package main

import (
    "fmt"
    "log"
    "net/url"
)

func main() {
    u, err := url.Parse("../../..//search?q=dotnet")
    if err != nil {
        log.Fatal(err)
    }
    base, err := url.Parse("http://example.com/directory/")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(base.ResolveReference(u))
}

Вывод

http://example.com/search?q=dotnet

Пример в песочнице https://play.golang.org/p/WGNUEkswSmY

Метод String

func (u *URL) String() string

String повторно собирает URL в допустимую строку URL-адреса. Общий вид результата - один из:

scheme:opaque?query#fragment
scheme://userinfo@host/path?query#fragment

Если u.Opaque не пусто, String использует первую форму; в противном случае используется вторая форма. Любые символы, отличные от ASCII, в host экранируются. Чтобы получить путь (path), String использует u.EscapedPath().

Во второй форме действуют следующие правила:

- если u.Scheme пуст, scheme: не отображается.
- если u.User равен nil, userinfo@ опускается.
- если u.Host пуст, host/ не указывается.
- если u.Scheme и u.Host пусты, а u.User - nil,
    вся часть scheme://userinfo@host/ опускается.
- если u.Host не пуст и u.Path начинается с /,
    форма host/path не добавляет свой /.
- если u.RawQuery пуст, ?query опускается.
- если u.Fragment пуст, #fragment опускается.

Пример использования метода String

package main

import (
    "fmt"
    "net/url"
)

func main() {
    u := &url.URL{
        Scheme:   "https",
        User:     url.UserPassword("me", "pass"),
        Host:     "example.com",
        Path:     "foo/bar",
        RawQuery: "x=1&y=2",
        Fragment: "anchor",
    }
    fmt.Println(u.String())
    u.Opaque = "opaque"
    fmt.Println(u.String())
}

Вывод

https://me:pass@example.com/foo/bar?x=1&y=2#anchor
https:opaque?x=1&y=2#anchor

Пример в песочнице https://play.golang.org/p/trgWI4TlgAP

Метод UnmarshalBinary

func (u *URL) UnmarshalBinary(text []byte) error

Преобразует срез байтов и наполняет поля структуры URL, на которой был вызван. Возвращает ошибку в случае ее возникновения при анализе среза байтов.

Пример использования метода UnmarshalBinary

package main

import (
    "fmt"
    "log"
    "net/url"
)

func main() {
    u := &url.URL{}
    err := u.UnmarshalBinary([]byte("https://example.org/foo"))
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("%s\n", u)
}

Вывод

https://example.org/foo

Пример в песочнице https://play.golang.org/p/SyeYQ-6_Vy4

Тип Values

type Values map[string][]string

Values сопоставляет строковый ключ со списком значений. Обычно он используется для параметров запроса и значений формы. В отличие от карты http.Header, ключи в карте Values чувствительны к регистру.

Пример использования типа Values

package main

import (
    "fmt"
    "net/url"
)

func main() {
    v := url.Values{}
    v.Set("name", "Ava")
    v.Add("friend", "Jess")
    v.Add("friend", "Sarah")
    v.Add("friend", "Zoe")
    fmt.Println(v.Encode())
    fmt.Println(v.Get("name"))
    fmt.Println(v.Get("friend"))
    fmt.Println(v["friend"])
    fmt.Println(v.Has("name"))
    v.Del("name")
    fmt.Println(v.Encode())
}

Вывод

friend=Jess&friend=Sarah&friend=Zoe&name=Ava
Ava
Jess
[Jess Sarah Zoe]
true
friend=Jess&friend=Sarah&friend=Zoe

Пример в песочнице https://play.golang.org/p/9Gvyq0Rnlx7

Функция ParseQuery

func ParseQuery(query string) (Values, error)

ParseQuery анализирует строку запроса в кодировке URL и возвращает карту, в которой перечислены значения, указанные для каждого ключа. ParseQuery всегда возвращает ненулевую карту, содержащую все найденные допустимые параметры запроса; err описывает первую обнаруженную ошибку декодирования, если таковая имеется.

Ожидается, что запрос будет списком настроек ключ = значение, разделенных амперсандами. Параметр без знака равенства интерпретируется как ключ, установленный на пустое значение. Параметры, содержащие точку с запятой без кодировки URL, считаются недействительными.

Пример использования функции ParseQuery

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/url"
    "strings"
)

func main() {
    m, err := url.ParseQuery(`x=1&y=2&y=3`)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(toJSON(m))
}

func toJSON(m interface{}) string {
    js, err := json.Marshal(m)
    if err != nil {
        log.Fatal(err)
    }
    return strings.ReplaceAll(string(js), ",", ", ")
}

Вывод

{"x":["1"], "y":["2", "3"]}

Пример в песочнице https://play.golang.org/p/3ytJDMl5P5y

Методы типа Values

func (v Values) Add(key, value string)

Add добавляет значение к ключу. Он добавляется к любым существующим значениям, связанным с ключом.

func (v Values) Del(key string)

Del удаляет значения, связанные с ключом.

func (v Values) Encode() string

Encode кодирует значения в форму "закодированный URL" ("bar=baz&foo=quux"), отсортированные по ключу.

func (v Values) Get(key string) string

Get получает первое значение, связанное с данным ключом. Если с ключом не связаны никакие значения, Get возвращает пустую строку. Чтобы получить доступ к нескольким значениям, используйте карту напрямую.

func (v Values) Has(key string) bool

Has проверяет, установлен ли данный ключ.

func (v Values) Set(key, value string)

Set устанавливает значение ключа. Он заменяет любые существующие значения.


Читайте также:


суббота, 11 сентября 2021 г.

Разработка RESTful API с помощью Go и Gin

В этом посте представлены основы написания API RESTful веб-сервиса с помощью Go и Gin Web Framework (Gin).

Вы получите максимальную отдачу от этого руководства, если у вас будут базовые знания Go и его инструментов. Если это ваше первое знакомство с Go, см. Начало работы с Go для быстрого ознакомления.

Gin упрощает многие задачи кодирования, связанные с созданием веб-приложений, включая веб-сервисы. В этом руководстве вы будете использовать Gin для маршрутизации запросов, получения сведений о запросах и маршалинга JSON для ответов.

В этом руководстве вы создадите RESTful API сервер с двумя конечными точками. Ваш примерный проект будет репозиторием данных о винтажных джазовых записях.

Предпосылки

1. Установка Go 1.16 или новее. Инструкции по установке см. Установка Go.

2. Инструмент для редактирования вашего кода. Любой текстовый редактор, который у вас есть, будет работать нормально.

3. Командный терминал. Go хорошо работает с любым терминалом в Linux и Mac, а также с PowerShell или cmd в Windows.

4. Инструмент curl. В Linux и Mac это уже должно быть установлено. В Windows он входит в состав Windows 10 . Для более ранних версий Windows может потребоваться его установка.

Проектирование конечных точек API

Вы создадите API, открывающий доступ к магазину, где продаются винтажные записи на виниле. Поэтому вам нужно будет предоставить конечные точки, через которые клиент может получать и добавлять альбомы для пользователей.

При разработке API вы обычно начинаете с проектирования конечных точек. Пользователи вашего API добьются большего успеха, если конечные точки будут простыми для понимания.

Вот конечные точки, которые вы создадите в этом руководстве.

/albums

  • GET - получить список всех альбомов в формате JSON.
  • POST - добавить новый альбом из данных запроса, отправленных в формате JSON.

/albums/:id

  • GET - получить альбом по его идентификатору, вернув данные альбома в формате JSON.

Далее вы создадите папку для своего кода.

Создайте папку для вашего кода

Для начала создайте проект для кода, который вы напишете.

1. Откройте командную строку и перейдите в свой домашний каталог.

В Linux или Mac:

$ cd

В Windows:

C:\> cd %HOMEPATH%

2. Используя командную строку, создайте каталог для вашего кода с именем web-service-gin.

$ mkdir web-service-gin
$ cd web-service-gin

3. Создайте модуль, в котором вы сможете управлять зависимостями.

Запустите команду go mod init, указав ей путь к модулю, в котором будет находиться ваш код.

$ go mod init example/web-service-gin
go: creating new go.mod: module example/web-service-gin

Эта команда создает файл go.mod, в котором будут перечислены добавленные вами зависимости для отслеживания. Дополнительные сведения об именовании модуля с помощью пути к модулю см. в посте Управление зависимостями.

Далее вы создадите структуры данных для обработки данных.

Создайте данные

Чтобы упростить работу с руководством, вы будете хранить данные в памяти. Более типичный API будет взаимодействовать с базой данных.

Обратите внимание, что хранение данных в памяти означает, что набор альбомов будет утерян каждый раз при остановке сервера, а затем воссоздан при его запуске.

Напишите код

Используя текстовый редактор, создайте файл с именем main.go в каталоге web-service-gin. Вы напишете свой код Go в этом файле.

В main.go в верхней части файла вставьте следующее объявление пакета.

package main

Автономная программа (в отличие от библиотеки) всегда находится в пакете main.

Под объявлением пакета вставьте следующее объявление структуры album. Вы будете использовать ее для хранения данных альбома в памяти.

Теги структуры, такие как json:"artist", определяют, каким должно быть имя поля, когда содержимое структуры сериализуется в JSON. Без них JSON использовал бы имена полей структуры с заглавной буквы - стиль, который не так распространен в JSON.

// album представляет данные об альбоме.
type album struct {
    ID     string  `json:"id"`
    Title  string  `json:"title"`
    Artist string  `json:"artist"`
    Price  float64 `json:"price"`
}

Под только что добавленным объявлением структуры вставьте следующий срез album структур, содержащий данные, которые вы будете использовать для начала.

// Срез albums для заполнения данных об альбомах.
var albums = []album{
    {ID: "1", Title: "Blue Train", Artist: "John Coltrane", Price: 56.99},
    {ID: "2", Title: "Jeru", Artist: "Gerry Mulligan", Price: 17.99},
    {ID: "3", Title: "Sarah Vaughan and Clifford Brown", Artist: "Sarah Vaughan", Price: 39.99},
}

Далее вы напишете код для реализации вашей первой конечной точки.

Напишите обработчик для возврата всех товаров

Когда клиент делает запрос в GET /albums, вы хотите вернуть все альбомы в формате JSON.

Для этого вам нужно написать следующее:

  • Логика для подготовки ответа
  • Код для сопоставления пути запроса с вашей логикой

Обратите внимание, что это противоположно тому, как они будут выполняться во время выполнения, но вы сначала добавляете зависимости, а затем код, который от них зависит.

Напишите код

Под кодом структур, который вы добавили в предыдущем разделе, вставьте следующий код, чтобы получить список альбомов.

Эта функция getAlbums создает JSON из среза album структур, записывая JSON в ответ.

// getAlbums возвращает список всех альбомов в формате JSON.
func getAlbums(c *gin.Context) {
    c.IndentedJSON(http.StatusOK, albums)
}

В этом коде вы:

  • Напишете функцию getAlbums, которая принимает параметр gin.Context. Обратите внимание, что вы могли дать этой функции любое имя - ни Gin, ни Go не требуют определенного формата имени функции.
    gin.Context - самая важная часть Gin. Он передает детали запроса, проверяет и сериализует JSON и многое другое. (Несмотря на похожее название, он отличается от встроенного в Go пакета context.)
  • Вызовете Context.IndentedJSON, чтобы сериализовать структуру в JSON и добавить ее в ответ.
    Первый аргумент функции - это код состояния HTTP, который вы хотите отправить клиенту. Здесь вы передаете константу StatusOK из пакета net/http, чтобы указать 200 OK.
    Обратите внимание, что вы можете заменить Context.IndentedJSON вызовом Context.JSON для отправки более компактного JSON. На практике с формой с отступом намного легче работать при отладке, и разница в размере обычно небольшая.

В верхней части main.go, сразу под объявлением среза albums, вставьте приведенный ниже код, чтобы назначить функцию-обработчик пути к конечной точке.

Это устанавливает связь, в которой getAlbums обрабатывает запросы к пути конечной точки /albums.

func main() {
    router := gin.Default()
    router.GET("/albums", getAlbums)

    router.Run("localhost:8080")
}

В этом коде вы:

  • Инициализируете роутер Gin, используя Default.
  • Используете функцию GET, чтобы связать метод GET HTTP и путь /albums с функцией обработчика.
    Обратите внимание, что вы передаете имя функции getAlbums. Это отличается от передачи результата функции, которую вы бы сделали, передав getAlbums() (обратите внимание на круглые скобки).
  • Используйте функцию Run, чтобы подключить маршрутизатор к http.Server и запустить сервер.

В верхней части main.go, сразу под объявлением пакета, импортируйте пакеты, которые вам понадобятся для поддержки кода, который вы только что написали.

Первые строки кода должны выглядеть так:

package main

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

Сохраните main.go.

Запустите код

Начните отслеживать модуль Gin как зависимость.

В командной строке используйте go get, чтобы добавить модуль github.com/gin-gonic/gin в качестве зависимости для вашего модуля. Используйте аргумент с точкой для обозначения "получить зависимости для кода в текущем каталоге".

$ go get .
go get: added github.com/gin-gonic/gin v1.7.2

Go разрешил и загрузил эту зависимость, чтобы удовлетворить декларацию импорта, которую вы добавили на предыдущем шаге.

Из командной строки в каталоге, содержащем main.go, запустите код. Используйте аргумент с точкой для обозначения "запустить код в текущем каталоге".

$ go run .

После запуска кода у вас есть работающий HTTP-сервер, на который вы можете отправлять запросы.

В новом окне командной строки используйте curl, чтобы сделать запрос к работающему веб-сервису.

$ curl http://localhost:8080/albums

Команда должна отображать данные, которыми вы заполнили сервис.

[
        {
                "id": "1",
                "title": "Blue Train",
                "artist": "John Coltrane",
                "price": 56.99
        },
        {
                "id": "2",
                "title": "Jeru",
                "artist": "Gerry Mulligan",
                "price": 17.99
        },
        {
                "id": "3",
                "title": "Sarah Vaughan and Clifford Brown",
                "artist": "Sarah Vaughan",
                "price": 39.99
        }
]

Вы запустили API! В следующем разделе вы создадите еще одну конечную точку с кодом для обработки POST запроса для добавления элемента.

Напишите обработчик для добавления нового элемента

Когда клиент отправляет запрос POST в /albums, вы хотите добавить альбом, описанный в теле запроса, к существующим данным альбомов.

Для этого вам нужно написать следующее:

  • Логика добавления нового альбома в существующий список.
  • Немного кода для маршрутизации запроса POST к вашей логике.

Напишите код

Добавьте код для добавления данных альбомов в список альбомов.

Где-нибудь после операторов импорта вставьте следующий код. (Конец файла - хорошее место для этого кода, но Go не обеспечивает соблюдение порядка, в котором вы объявляете функции.)

// postAlbums добавляет альбом из JSON, 
// полученного в теле запроса.
func postAlbums(c *gin.Context) {
    var newAlbum album

    // Вызов BindJSON для привязки 
    // полученного JSON к newAlbum
    if err := c.BindJSON(&newAlbum); err != nil {
        return
    }

    // Добавляем в срез новый альбом.
    albums = append(albums, newAlbum)
    c.IndentedJSON(http.StatusCreated, newAlbum)
}

В этом коде вы:

  • Используйте Context.BindJSON для привязки тела запроса к newAlbum.
  • Добавляете структуру album, инициализированную из JSON, в срез albums.
  • Добавляете в ответ код состояния 201 вместе с JSON, представляющим добавленный вами альбом.

Измените main функцию так, чтобы она включала функцию router.POST, как показано ниже.

func main() {
    router := gin.Default()
    router.GET("/albums", getAlbums)
    router.POST("/albums", postAlbums)

    router.Run("localhost:8080")
}

В этом коде вы:

  • Связываете метод POST по пути /albums с функцией postAlbums.
    С помощью Gin вы можете связать обработчик с комбинацией HTTP-метода и пути. Таким образом, вы можете отдельно маршрутизировать запросы, отправленные по одному пути, в зависимости от метода, который использует клиент.

Запустите код

Если сервер все еще работает с последнего раздела, остановите его.

Из командной строки в каталоге, содержащем main.go, запустите код.

$ go run .

Из другого окна командной строки используйте curl, чтобы сделать запрос к работающему веб-сервису.

$ curl http://localhost:8080/albums \
    --include \
    --header "Content-Type: application/json" \
    --request "POST" \
    --data '{"id": "4","title": "The Modern Sound of Betty Carter","artist": "Betty Carter","price": 49.99}'

Команда должна отображать заголовки и JSON для добавленного альбома.

HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Date: Wed, 02 Jun 2021 00:34:12 GMT
Content-Length: 116

{
    "id": "4",
    "title": "The Modern Sound of Betty Carter",
    "artist": "Betty Carter",
    "price": 49.99
}

Как и в предыдущем разделе, используйте curl для получения полного списка альбомов, который вы можете использовать для подтверждения того, что новый альбом был добавлен.

$ curl http://localhost:8080/albums \
    --header "Content-Type: application/json" \
    --request "GET"

Команда должна отобразить список альбомов.

[
        {
                "id": "1",
                "title": "Blue Train",
                "artist": "John Coltrane",
                "price": 56.99
        },
        {
                "id": "2",
                "title": "Jeru",
                "artist": "Gerry Mulligan",
                "price": 17.99
        },
        {
                "id": "3",
                "title": "Sarah Vaughan and Clifford Brown",
                "artist": "Sarah Vaughan",
                "price": 39.99
        },
        {
                "id": "4",
                "title": "The Modern Sound of Betty Carter",
                "artist": "Betty Carter",
                "price": 49.99
        }
]

В следующем разделе вы добавите код для обработки GET для определенного элемента.

Напишите обработчик для получения определенного элемента

Когда клиент делает запрос к GET /album/[id], вы хотите вернуть альбом, идентификатор которого совпадает с параметром пути id.

Для этого вы:

  • Добавите логику для получения запрошенного альбома.
  • Укажите путь к логике.

Напишите код

Под функцией postAlbums, которую вы добавили в предыдущем разделе, вставьте следующий код для получения определенного альбома.

Эта функция getAlbumByID извлечет идентификатор из пути запроса, а затем найдет соответствующий альбом.

// getAlbumByID находит альбом, 
// значение идентификатора которого совпадает с 
// параметром id, отправленным клиентом,
// затем возвращает этот альбом в качестве ответа
func getAlbumByID(c *gin.Context) {
    id := c.Param("id")

    // Перебираем список альбомов в поисках альбома,
    // значение идентификатора которого соответствует параметру.
    for _, a := range albums {
        if a.ID == id {
            c.IndentedJSON(http.StatusOK, a)
            return
        }
    }
    c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"})
}

В этом коде вы:

  • Используете Context.Param, чтобы получить параметр пути id из URL-адреса. Когда вы сопоставляете этот обработчик с путем, вы включаете в путь заполнитель для параметра.
  • Проходите в цикле по структурам album в срезе, ища ту, значение поля ID которой совпадает со значением параметра id. Если он найден, вы сериализуете эту album структуру в JSON и возвращаете ее в качестве ответа с HTTP-кодом 200 OK.
    Как упоминалось выше, реальный сервис, скорее всего, будет использовать запрос к базе данных для выполнения этого поиска.
  • Вернуть ошибку HTTP 404 с http.StatusNotFound, если альбом не найден.

Наконец, измените свой main так, чтобы он включал новый вызов router.GET, где теперь путь - /albums/:id, как показано в следующем примере.

func main() {
    router := gin.Default()
    router.GET("/albums", getAlbums)
    router.GET("/albums/:id", getAlbumByID)
    router.POST("/albums", postAlbums)

    router.Run("localhost:8080")
}

В этом коде вы:

  • Связываете путь /albums/:id с функцией getAlbumByID. В Gin двоеточие перед элементом в пути означает, что этот элемент является параметром пути.

Запустите код

Если сервер все еще работает с последнего раздела, остановите его.

Из командной строки в каталоге, содержащем main.go, запустите код, чтобы запустить сервер.

$ go run .

Из другого окна командной строки используйте curl, чтобы сделать запрос к работающему веб-сервису.

$ curl http://localhost:8080/albums/2

Команда должна отображать JSON для альбома, идентификатор которого вы использовали. Если альбом не найден, вы получите JSON с сообщением об ошибке.

{
        "id": "2",
        "title": "Jeru",
        "artist": "Gerry Mulligan",
        "price": 17.99
}

Заключение

Вы только что использовали Go и Gin для написания простого RESTful веб-сервиса.

Итоговый код

В этом разделе содержится код приложения, которое вы создаете с помощью этого руководства.

package main

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

// album представляет данные об альбоме.
type album struct {
    ID     string  `json:"id"`
    Title  string  `json:"title"`
    Artist string  `json:"artist"`
    Price  float64 `json:"price"`
}

// Срез albums для заполнения данных об альбомах.
var albums = []album{
    {ID: "1", Title: "Blue Train", Artist: "John Coltrane", Price: 56.99},
    {ID: "2", Title: "Jeru", Artist: "Gerry Mulligan", Price: 17.99},
    {ID: "3", Title: "Sarah Vaughan and Clifford Brown", Artist: "Sarah Vaughan", Price: 39.99},
}

func main() {
    router := gin.Default()
    router.GET("/albums", getAlbums)
    router.GET("/albums/:id", getAlbumByID)
    router.POST("/albums", postAlbums)

    router.Run("localhost:8080")
}

// getAlbums возвращает список всех альбомов в формате JSON.
func getAlbums(c *gin.Context) {
    c.IndentedJSON(http.StatusOK, albums)
}

// postAlbums добавляет альбом из JSON, полученный в теле запроса.
func postAlbums(c *gin.Context) {
    var newAlbum album

    // Вызов BindJSON для привязки 
    // полученного JSON к newAlbum
    if err := c.BindJSON(&newAlbum); err != nil {
        return
    }

    // Добавляем в срез новый альбом.
    albums = append(albums, newAlbum)
    c.IndentedJSON(http.StatusCreated, newAlbum)
}

// getAlbumByID находит альбом, 
// значение идентификатора которого совпадает с 
// параметром id, отправленным клиентом,
// затем возвращает этот альбом в качестве ответа
func getAlbumByID(c *gin.Context) {
    id := c.Param("id")

    // Перебираем список альбомов в поисках альбома,
    // значение идентификатора которого соответствует параметру.
    for _, a := range albums {
        if a.ID == id {
            c.IndentedJSON(http.StatusOK, a)
            return
        }
    }
    c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"})
}


Читайте также: