четверг, 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"})
}


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