🐹 Go

Полный справочник: модули, типы, срезы и map, функции и ошибки, структуры и интерфейсы, дженерики, горутины и каналы, context, net/http, JSON, тесты и инструменты.

Шпаргалки · Программирование · #go #golang #backend #concurrency

Инструменты

go version                  go mod init example.com/app          go mod tidy          go get github.com/x/y@latest
go run .                    go build -o app ./cmd/app            go install ./...     go vet ./...
go test ./...               go test -race -cover ./...           go test -bench . -benchmem
gofmt -w .                  go fmt ./...                          golangci-lint run
GOOS=linux GOARCH=amd64 go build                                  # кросс-компиляция

IDE: GoLand, VS Code (расширение Go + gopls). Документация: go doc fmt.Println, pkg.go.dev. Структура: cmd/ (точки входа), internal/ (закрытый код), pkg/ (публичный).

Минимальная программа

package main

import "fmt"

func main() {
    fmt.Println("Привет")
}

Файлы начинаются с package. Экспортируется то, что с заглавной буквы (Name), остальное закрыто в пакете.

Переменные и типы

var n int = 5          var s string          // нулевые значения: 0, "", false, nil
x := 10                                       // короткое объявление (только внутри функций)
const Pi = 3.14        const ( A = iota; B; C )                  // iota: 0, 1, 2
var a, b = 1, "x"
Группа Типы
Целые int, int8…int64, uint, uint8…uint64, uintptr; byte = uint8, rune = int32
Дробные float32, float64, complex64/128
Прочие bool, string, error, any (interface{})
Составные массив [3]int, срез []int, map[K]V, struct, *T, func, chan T
float64(n); int(f); strconv.Itoa(5); strconv.Atoi("5"); strconv.ParseFloat("1.5", 64); fmt.Sprint(n)
type ID int64             type Celsius float64           // именованные типы

Приведение явное: Go не расширяет типы неявно.

Строки

s := "Привет"
len(s)                       // байты (UTF-8), не символы
utf8.RuneCountInString(s)    // символы
for i, r := range s {}        // r — rune
[]rune(s); []byte(s); string(runes)
strings.Contains(s, "р"); strings.Split(s, ","); strings.Join(parts, ","); strings.TrimSpace(s); strings.ToUpper(s)
strings.Fields(s); strings.Replace(s, "a", "b", -1); strings.HasPrefix(s, "П"); strings.Builder
fmt.Sprintf("%s: %d (%.2f) %v %+v %T %q %x", name, 5, 3.14159, v, v, v, s, 255)

%v значение, %+v с именами полей, %#v синтаксис Go, %T тип, %q в кавычках, %x hex. Строки неизменяемы; для сборки strings.Builder.

Управление

if x > 0 { } else if x == 0 { } else { }
if v, ok := m["key"]; ok { }                  // инициализация в if
switch day { case "sat", "sun": ...; case "mon": ...; default: ... }
switch { case x < 0: ...; case x == 0: ...; default: ... }          // без выражения; нет «проваливания»
switch v := i.(type) { case int: ...; case string: ...; }           // switch по типу
for i := 0; i < 3; i++ { }            for cond { }           for { }            // единственный цикл
for i, v := range slice { }           for k, v := range m { }       for i := range 10 { }     // range по числу (1.22)
for range ch { }                      break; continue; goto; label: for ...
defer f.Close()                       // выполнится при выходе из функции (в обратном порядке)

Массивы, срезы, map

arr := [3]int{1, 2, 3}                       // массив фиксированной длины, копируется по значению
sl := []int{1, 2, 3}                          sl = append(sl, 4, 5)                  sl2 := sl[1:3]            // срез ссылается на тот же массив
make([]int, 0, 100)                           len(sl); cap(sl); copy(dst, src); slices.Contains(sl, 2); slices.Sort(sl); slices.Index(sl, 3)
sl = slices.Delete(sl, 1, 2);  sl = append(sl[:i], sl[i+1:]...)
m := map[string]int{"a": 1}                  m["b"] = 2          v, ok := m["x"]        delete(m, "a")       len(m)
make(map[string][]string)                     maps.Keys(m); maps.Clone(m)          // map не потокобезопасен

Подвох: срезы делят память, append может переиспользовать массив. Чтобы изолировать: slices.Clone(sl). Порядок обхода map случайный. Запись в nil-map паникует, чтение нет.

Функции и ошибки

func add(a, b int) int { return a + b }
func div(a, b float64) (float64, error) {
    if b == 0 { return 0, errors.New("деление на ноль") }
    return a / b, nil
}
func sum(nums ...int) int { }            sum(sl...)
f := func(x int) int { return x * x }    // замыкание
func named() (n int, err error) { n = 1; return }

res, err := div(1, 0)
if err != nil { return fmt.Errorf("div: %w", err) }                       // обёртка ошибки
errors.Is(err, ErrNotFound); errors.As(err, &target); errors.Join(e1, e2)
var ErrNotFound = errors.New("не найдено")
type MyErr struct{ Code int }; func (e *MyErr) Error() string { return fmt.Sprint(e.Code) }
panic("критично")                         defer func() { if r := recover(); r != nil { } }()    // для невосстановимых ситуаций

Идиома: ошибка последнее возвращаемое значение, обрабатывайте её сразу.

Структуры, методы, интерфейсы

type User struct {
    ID    int    `json:"id" db:"id"`
    Name  string `json:"name,omitempty"`
    email string                                    // с маленькой — не экспортируется
}
u := User{ID: 1, Name: "Аня"}       p := &u        p.Name = "Б"       // доступ через указатель без разыменования

func (u User) Label() string { return "@" + u.Name }          // метод со значением
func (u *User) Rename(n string) { u.Name = n }                 // метод с указателем: изменяет

type Reader interface { Read(p []byte) (int, error) }          // интерфейс: неявная реализация
type Shape interface { Area() float64 }
var s Shape = Circle{R: 1}
type Admin struct { User; Level int }                           // встраивание (композиция): Admin получает методы User

Принимайте интерфейсы, возвращайте структуры. Мелкие интерфейсы (io.Reader, fmt.Stringer). Пустой интерфейс any теряет типизацию.

Дженерики (1.18+)

func Map[T, U any](xs []T, f func(T) U) []U { out := make([]U, 0, len(xs)); for _, x := range xs { out = append(out, f(x)) }; return out }
type Number interface { ~int | ~int64 | ~float64 }
func Sum[T Number](xs []T) (s T) { for _, x := range xs { s += x }; return }
type Stack[T any] struct{ items []T }
func (s *Stack[T]) Push(x T) { s.items = append(s.items, x) }

Ограничения: any, comparable, cmp.Ordered.

Горутины и каналы

go work()                                      // запуск лёгкой горутины
ch := make(chan int)      buf := make(chan int, 10)         ch <- 1         v := <-ch         close(ch)         v, ok := <-ch
for v := range ch { }                          // до закрытия канала

var wg sync.WaitGroup
for i := 0; i < 5; i++ { wg.Add(1); go func() { defer wg.Done(); work(i) }() }
wg.Wait()

var mu sync.Mutex;  mu.Lock(); defer mu.Unlock()               // sync.RWMutex, sync.Once, sync/atomic
select {
case v := <-ch: fmt.Println(v)
case <-time.After(time.Second): fmt.Println("таймаут")
case <-ctx.Done(): return ctx.Err()
default: }

Правила: канал закрывает отправитель; не запускайте горутины без способа остановки (утечки); при общей памяти используйте -race. Пул воркеров: канал задач + WaitGroup. Для группы с ошибками: golang.org/x/sync/errgroup.

context

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
select { case <-ctx.Done(): return ctx.Err() }
ctx = context.WithValue(ctx, key{}, "val")                     // для данных запроса, не для опций

Передавайте ctx первым параметром во все функции, делающие ввод-вывод.

Работа с HTTP и JSON

// сервер
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {      // шаблоны с методом и параметром (1.22)
    id := r.PathValue("id")
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]any{"id": id})
})
srv := &http.Server{Addr: ":8080", Handler: mux, ReadHeaderTimeout: 5 * time.Second}
log.Fatal(srv.ListenAndServe())

// клиент
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Get(url); if err != nil { return err }; defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)

var u User; json.Unmarshal(data, &u); json.Marshal(u); json.NewDecoder(r.Body).Decode(&u)

Не используйте http.DefaultClient без таймаута.

Файлы и ввод-вывод

data, err := os.ReadFile("a.txt");   os.WriteFile("b.txt", data, 0o644);   os.MkdirAll(dir, 0o755);   os.Remove(p)
f, _ := os.Open("a.txt"); defer f.Close(); sc := bufio.NewScanner(f); for sc.Scan() { sc.Text() }
filepath.Join("a", "b"); filepath.Walk(root, fn); os.Getenv("HOME"); os.Args; flag.String("port", "8080", "порт"); flag.Parse()

Логи

log/slog (структурные): slog.Info("start", "port", 8080), slog.New(slog.NewJSONHandler(os.Stdout, nil)).

Тесты

func TestAdd(t *testing.T) { if got := add(1, 2); got != 3 { t.Errorf("got %d, want 3", got) } }
func TestTable(t *testing.T) {
    tests := []struct{ name string; a, b, want int }{{"plus", 1, 2, 3}, {"zero", 0, 0, 0}}
    for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if add(tt.a, tt.b) != tt.want { t.Fatal("fail") } }) }
}
func BenchmarkAdd(b *testing.B) { for i := 0; i < b.N; i++ { add(1, 2) } }
func FuzzParse(f *testing.F) { f.Fuzz(func(t *testing.T, s string) { parse(s) }) }

Файлы *_test.go. Библиотеки: testify, gomock, httptest (встроено), testcontainers-go.

Подводные камни

  • Горутины в цикле захватывали переменную до 1.22; сейчас каждая итерация своя, но следите за версией go в go.mod.
  • nil-интерфейс против интерфейса с nil-указателем.
  • Утечки горутин и незакрытое тело ответа.
  • Гонки данных (go test -race).
  • Копия sync.Mutex по значению.
  • Срезы делят память.

Библиотеки по задачам

Задача Что взять
Веб net/http, chi, Gin, Echo, Fiber
БД database/sql + pgx, sqlx, GORM, sqlc, ent
Миграции goose, golang-migrate, atlas
CLI cobra, urfave/cli, pflag
Конфиги viper, envconfig, koanf
gRPC google.golang.org/grpc, connect-go
Очереди sarama, franz-go (Kafka), amqp091-go
Логи slog, zap, zerolog
Валидация go-playground/validator
Метрики prometheus/client_golang, OpenTelemetry