Skip to content

Go Шпаргалка

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

01

Начало работы

Hello World

Каждая программа Go начинается в функции main() пакета main. Используйте 'go run' для выполнения, 'go build' для компиляции. gofmt автоматически форматирует код (табы, интервалы). Оператор import подключает пакеты — fmt обрабатывает форматированный I/O.

go
package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
    fmt.Printf("Name: %s, Age: %d\n", "Alice", 30)
}

// Run: go run main.go
// Build: go build -o app main.go
// Format: gofmt -w main.go

Go Modules (go.mod)

Go modules (go.mod) управляют зависимостями с Go 1.11. 'go mod init' создаёт файл модуля. 'go get' добавляет зависимости. 'go mod tidy' удаляет неиспользуемые импорты и добавляет отсутствующие. Путь модуля — путь импорта для вашего пакета.

go
// Initialize a new module
// $ go mod init github.com/user/project

// go.mod file:
module github.com/user/project

go 1.21

require (
    github.com/gin-gonic/gin v1.9.1
    golang.org/x/sync v0.3.0
)

// Add dependencies:
// $ go get github.com/gin-gonic/gin
// $ go mod tidy  // clean up unused deps

Структура пакетов

Go организует код в пакетах — один пакет на каталог. Имена с заглавной буквы (Println, Add) экспортируемые (публичные); строчные имена (приватные) — package-private. Каталог 'internal' ограничивает импорты родительским модулем. Имя пакета должно совпадать с именем каталога.

go
// project structure:
// myproject/
//   go.mod
//   main.go          // package main
//   utils/
//     helper.go      // package utils
//     math.go
//   internal/
//     api/           // private (only importable within parent)

// main.go
package main

import (
    "fmt"
    "github.com/user/project/utils"
)

func main() {
    fmt.Println(utils.Add(1, 2))
}

// Exported names start with uppercase letter
// unexported names start with lowercase letter

Переменные и константы

Используйте 'var name type = value' для явных объявлений, 'name := value' для коротких объявлений (только функции). В Go есть нулевые значения: 0 для чисел, '' для строк, false для булевых, nil для указателей/срезов/мап. Константы (const) — compile-time и не могут использовать :=.

go
package main

import "fmt"

// Package-level declarations
var version = "1.0.0" // inferred type
const Pi = 3.14159

func main() {
    // var with type
    var name string = "Alice"
    var age int = 30

    // Short declaration (only inside functions)
    city := "NYC"

    // Multiple assignment
    a, b, c := 1, 2, 3

    // Zero values (default)
    var x int      // 0
    var s string   // ""
    var ok bool    // false
    var p *int     // nil

    fmt.Println(name, age, city, a, b, c, x, s, ok, p)
}

Базовые типы и преобразование

Go требует явного преобразования типов — нет неявного, как в C. rune — псевдоним для int32 (представляет Unicode code point). byte — псевдоним для uint8. Преобразование между числовыми типами может терять точность (float в int усекает). string(65) преобразует code point в символ.

go
// Numeric types
var i int = 42
var i8 int8 = 127
var u uint = 42
var f32 float32 = 3.14
var f64 float64 = 3.141592653589793

// Other types
var b bool = true
var s string = "hello"
var r rune = 'A' // int32 alias for Unicode code point
var by byte = 255 // uint8 alias

// Type conversion (explicit, no implicit conversion)
var n int = 42
var f float64 = float64(n)  // int -> float64
var s string = string(65)   // 65 -> "A" (rune to string)
var i2 int = int(f)         // float64 -> int (truncates)

// Type inference
var x = 42        // int
var y = 3.14      // float64
var z = len("hi") // int
02

Строки и форматирование

Основы строк

Строки Go — неизменяемые последовательности байт. len() возвращает количество байт, а не символов — используйте utf8.RuneCountInString() для Unicode. Строки сравниваются лексикографически через ==, <, >. Range по строке итерирует по rune (Unicode code point), корректно обрабатывая многобайтовые символы.

go
package main

import (
    "fmt"
    "strings"
)

func main() {
    s := "Hello, World"

    // Length (bytes, not runes!)
    fmt.Println(len(s)) // 12

    // Concatenation
    s1 := "Hello" + " " + "World"
    s2 := fmt.Sprintf("%s = %d", "age", 30)

    // Comparison
    fmt.Println("abc" == "abc") // true
    fmt.Println("a" < "b")      // true (lexicographic)

    // Iteration (by rune)
    for i, r := range "Go语" {
        fmt.Printf("%d: %c\n", i, r)
    }
}

Пакет strings

Пакет strings предоставляет общие строковые операции. Contains/HasPrefix/HasSuffix проверяют подстроки. Split разбивает по разделителю; Join объединяет. Replace принимает счёт (-1 для всех). TrimSpace удаляет ведущие/завершающие пробелы. Все функции возвращают новые строки (строки неизменяемы).

go
import (
    "fmt"
    "strings"
)

s := "Hello, World"

// Case
strings.ToUpper(s)         // "HELLO, WORLD"
strings.ToLower(s)         // "hello, world"
strings.Title("hello world") // "Hello World"

// Search
strings.Contains(s, "World")  // true
strings.HasPrefix(s, "Hello") // true
strings.HasSuffix(s, "World") // true
strings.Index(s, "World")     // 7 (-1 if not found)
strings.Count(s, "l")         // 3

// Split & Join
parts := strings.Split("a,b,c", ",")  // ["a", "b", "c"]
joined := strings.Join(parts, "-")    // "a-b-c"

// Replace
strings.Replace(s, "l", "L", 1)  // "HeLlo, World" (1 occurrence)
strings.ReplaceAll(s, "l", "L")  // "HeLLo, WorLd"
strings.TrimSpace("  hi  ")      // "hi"

strconv и форматирование

strconv преобразует между строками и числами — всегда проверяйте возврат ошибки. fmt.Printf форматирует вывод: %d (int), %f (float), %s (string), %t (bool), %T (type), %x (hex), %q (в кавычках). Используйте %.2f для 2 знаков, %05d для заполнения нулями. Sprintf возвращает строку вместо печати.

go
import (
    "fmt"
    "strconv"
)

// String to number
n, err := strconv.Atoi("42")     // string -> int
f, err := strconv.ParseFloat("3.14", 64)

// Number to string
s1 := strconv.Itoa(42)            // int -> string
s2 := strconv.FormatFloat(3.14, 'f', 2, 64) // "3.14"

// Printf formatting
fmt.Printf("Int: %d\n", 42)
fmt.Printf("Float: %.2f\n", 3.14159)
fmt.Printf("String: %s\n", "hi")
fmt.Printf("Bool: %t\n", true)
fmt.Printf("Type: %T\n", 42)     // int
fmt.Printf("Hex: %x\n", 255)     // ff
fmt.Printf("Pad: %05d\n", 42)    // 00042
fmt.Printf("Quote: %q\n", "hi")  // "hi"

// Sprintf returns formatted string
result := fmt.Sprintf("Name: %s, Age: %d", "Alice", 30)

Сырые строки и многострочность

Сырые строки (обратные кавычки) сохраняют всё буквально — без escape-последовательностей, могут занимать несколько строк. Используйте для regex, SQL, HTML-шаблонов. Интерпретируемые строки (двойные кавычки) обрабатывают \n, \t и т.д. Для эффективной конкатенации строк в циклах используйте strings.Builder (избегает O(n²) выделения).

go
// Raw string literal (backticks) - no escape processing
raw := `This is a
multiline string
with \n (literal backslash-n)`

// Interpreted string (double quotes) - processes escapes
interpreted := "Line1\nLine2\tTabbed"

// Raw strings are useful for:
// - Regex patterns
// - HTML/SQL templates
// - File paths on Windows
regex := `^d{4}-d{2}-d{2}$`

// String builder (efficient concatenation)
var sb strings.Builder
for i := 0; i < 1000; i++ {
    sb.WriteString("line\n")
}
result := sb.String()

Unicode и Runes

Строки Go — последовательности байт в кодировке UTF-8. len() даёт байты; utf8.RuneCountInString() даёт символы. Range по строке автоматически декодирует UTF-8. Для модификации строки преобразуйте в []rune, измените и преобразуйте обратно. Это необходимо для интернационализированной обработки текста.

go
import (
    "fmt"
    "unicode/utf8"
)

s := "Hello, 世界"

// Byte length vs rune length
fmt.Println(len(s))                    // 13 (bytes)
fmt.Println(utf8.RuneCountInString(s)) // 9 (runes/characters)

// Decode runes manually
for i := 0; i < len(s); {
    r, size := utf8.DecodeRuneInString(s[i:])
    fmt.Printf("%c ", r)
    i += size
}

// Range automatically decodes runes
for i, r := range s {
    fmt.Printf("%d:%c ", i, r)
}

// Rune slice (mutable)
runes := []rune("Hello")
runes[0] = 'J'
s2 := string(runes) // "Jello"
03

Управление потоком

If / Else

if в Go не требует скобок вокруг условий, но фигурные скобки обязательны (даже для однострочных тел). Init-оператор (if x := f(); x > 0) ограничен блоком if/else — распространён для проверки ошибок. Этот паттерн держит область видимости переменной узкой.

go
// Basic if/else
score := 85
if score >= 90 {
    fmt.Println("A")
} else if score >= 80 {
    fmt.Println("B")
} else {
    fmt.Println("C")
}

// If with initialization statement
if n := computeValue(); n > 100 {
    fmt.Println("big:", n)
} else {
    fmt.Println("small:", n)
}
// n is not accessible here (scoped to if)

// No parentheses needed, but braces are required

Циклы for

В Go только одно ключевое слово цикла: 'for'. Оно обрабатывает C-стиль, while-стиль (for cond), бесконечный (for) и итерацию (for range). Range работает со срезами, мапами, строками и каналами. Используйте _ для пропуска индекса или значения. Порядок итерации мапы случаен по дизайну.

go
// C-style for
for i := 0; i < 5; i++ {
    fmt.Println(i)
}

// While-style (condition only)
n := 10
for n > 0 {
    n--
}

// Infinite loop
for {
    break
}

// Range (iterate slices, maps, strings)
nums := []int{10, 20, 30}
for index, value := range nums {
    fmt.Printf("%d: %d\n", index, value)
}

// Skip index or value with _
for _, value := range nums {
    fmt.Println(value)
}

// Range over map (unordered!)
m := map[string]int{"a": 1, "b": 2}
for key, val := range m {
    fmt.Println(key, val)
}

Switch

switch в Go не проваливается по умолчанию (в отличие от C/Java) — каждый case — отдельная ветка. Используйте fallthrough для принуждения. Несколько значений на case через запятые. switch без выражения работает как более чистая цепочка if/else. switch с init-оператором ограничивает переменную switch.

go
// Basic switch
day := 3
switch day {
case 1:
    fmt.Println("Mon")
case 2, 3, 4, 5:
    fmt.Println("Weekday")
case 6, 7:
    fmt.Println("Weekend")
default:
    fmt.Println("Invalid")
}

// Switch with no expression (like if/else chain)
switch {
case score >= 90:
    grade = "A"
case score >= 80:
    grade = "B"
default:
    grade = "C"
}

// Switch with init
switch os := runtime.GOOS; os {
case "linux":
    fmt.Println("Linux")
case "darwin":
    fmt.Println("macOS")
}

// Fallthrough (rare, goes to next case unconditionally)
switch 1 {
case 1:
    fmt.Println("one")
    fallthrough
case 2:
    fmt.Println("two") // executes even though value is 1
}

Defer

defer планирует вызов функции на возврат из объемлющей функции — LIFO (последний-первый). Используйте для очистки (закрытие файлов, снятие блокировок, закрытие подключений). Аргументы отложенных вызовов вычисляются немедленно, но вызов выполняется при возврате. Defers выполняются даже при панике функции.

go
// Defer runs when function returns (LIFO order)
func main() {
    defer fmt.Println("third")
    defer fmt.Println("second")
    fmt.Println("first")
}
// Output: first, second, third

// Common: resource cleanup
func readFile(path string) error {
    f, err := os.Open(path)
    if err != nil {
        return err
    }
    defer f.Close() // runs when readFile returns

    // ... use f ...
    return nil
}

// Defer evaluates arguments immediately
i := 1
defer fmt.Println(i) // prints 1 (not 2)
i = 2

Goto, Break, Continue

break выходит из внутреннего цикла; continue переходит к следующей итерации. Метки (break outer) выходят из вложенных циклов. goto существует, но редко используется — предпочитайте структурированный поток управления. Метки также можно использовать с continue для перехода к следующей итерации внешнего цикла.

go
// Break and continue
for i := 0; i < 10; i++ {
    if i%2 == 0 {
        continue // skip even numbers
    }
    if i > 7 {
        break // exit loop
    }
    fmt.Println(i)
}

// Break with label (for nested loops)
outer:
for i := 0; i < 3; i++ {
    for j := 0; j < 3; j++ {
        if i == 1 && j == 1 {
            break outer // exits both loops
        }
    }
}

// Goto (rarely used, avoid)
i := 0
loop:
    if i < 5 {
        fmt.Println(i)
        i++
        goto loop
    }

Select (операции с каналами)

select — как switch для каналов — ждёт несколько операций канала и выбирает первую готовую. default делает его неблокирующим. Используйте select в цикле для событийно-ориентированных паттернов. time.After() создаёт канал таймаута. select выбирает случайно, если готовы несколько case, предотвращая starvation.

go
// Select waits on multiple channel operations
select {
case msg := <-messages:
    fmt.Println("received:", msg)
case <-timeout:
    fmt.Println("timed out")
default:
    fmt.Println("no activity") // non-blocking
}

// Select with send
ch1 := make(chan int)
ch2 := make(chan int)
go func() { ch1 <- 1 }()
go func() { ch2 <- 2 }()

select {
case v := <-ch1:
    fmt.Println("ch1:", v)
case v := <-ch2:
    fmt.Println("ch2:", v)
}

// Select in a loop (event loop pattern)
for {
    select {
    case msg := <-ch:
        fmt.Println(msg)
    case <-time.After(5 * time.Second):
        return // timeout after 5s of inactivity
    }
}
04

Функции

Определение и множественные возвраты

Функции Go могут возвращать несколько значений — канонический паттерн (result, error). Именованные возвраты действуют как объявленные переменные, инициализированные нулевыми значениями; 'голый' return использует их. Именованные возвраты улучшают читаемость для сложных функций, но могут запутать при чрезмерном использовании. Всегда проверяйте ошибки немедленно.

go
// Function with multiple return values
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("divide by zero")
    }
    return a / b, nil
}

// Named return values
func split(sum int) (x, y int) {
    x = sum * 4 / 9
    y = sum - x
    return // "naked" return (uses named values)
}

// Usage
result, err := divide(10, 2)
if err != nil {
    log.Fatal(err)
}
fmt.Println(result) // 5

x, y := split(100)
fmt.Println(x, y)

Variadic и замыкания

Variadic-функции (...T) принимают любое количество аргументов, получаемых как срез. Разворачивайте срез через .... Замыкания захватывают переменные по ссылке из объемлющей области — функция counter() возвращает замыкание, которое запоминает 'count'. Замыкания полезны для callback'ов, итераторов и функций с состоянием.

go
// Variadic function (variable arguments)
func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}
fmt.Println(sum(1, 2, 3))       // 6
fmt.Println(sum(1, 2, 3, 4, 5)) // 15

// Spread a slice
nums := []int{1, 2, 3}
fmt.Println(sum(nums...)) // 6

// Closure (function that captures variables)
func counter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

next := counter()
fmt.Println(next()) // 1
fmt.Println(next()) // 2
fmt.Println(next()) // 3

Функции как значения

Функции в Go — значения первого класса — их можно назначать переменным, передавать как аргументы и хранить в структурах данных. Определяйте функциональные типы через 'type Name func(params) returns'. Мапы функций полезны для таблиц диспетчеризации, обработчиков команд и паттернов strategy.

go
// Functions are first-class values
func apply(f func(int) int, x int) int {
    return f(x)
}

func double(x int) int { return x * 2 }
func square(x int) int { return x * x }

fmt.Println(apply(double, 5)) // 10
fmt.Println(apply(square, 5)) // 25

// Function type
type MathFunc func(int) int

var fn MathFunc = func(x int) int { return x + 1 }
fmt.Println(fn(10)) // 11

// Map of functions
ops := map[string]func(int, int) int{
    "add": func(a, b int) int { return a + b },
    "sub": func(a, b int) int { return a - b },
    "mul": func(a, b int) int { return a * b },
}
fmt.Println(ops["add"](3, 4)) // 7

init() и анонимные функции

Функции init() запускаются автоматически до main(), в порядке объявления. Используйте для настройки (загрузка конфига, валидация, регистрация). Несколько init() на файл разрешены. Анонимные функции можно немедленно вызывать (IIFE) или назначать переменным. Также используются для горутин.

go
// init() runs before main(), once per package
// Multiple init() functions allowed, run in order declared
package main

import "fmt"

var config string

func init() {
    // Setup code: config, connections, validation
    config = "production"
    fmt.Println("init 1")
}

func init() {
    fmt.Println("init 2, config:", config)
}

func main() {
    fmt.Println("main")
}
// Output: init 1, init 2, main

// Anonymous function (IIFE)
result := func(x int) int {
    return x * 2
}(5) // immediately invoked, result = 10

// Goroutine with anonymous function
go func(msg string) {
    fmt.Println(msg)
}("async")

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

Go 1.18+ поддерживает дженерики с параметрами типа [T any]. Ограничения типа (интерфейсы) ограничивают разрешённые типы — используйте 'any' без ограничений, 'comparable' для == / != операторов. Union-ограничения (int | float64) разрешают конкретные типы. Дженерики обеспечивают type-safe переиспользуемые структуры данных и алгоритмы без дублирования кода.

go
// Generic function with type parameter
func Map[T, U any](slice []T, f func(T) U) []U {
    result := make([]U, len(slice))
    for i, v := range slice {
        result[i] = f(v)
    }
    return result
}

nums := []int{1, 2, 3}
doubled := Map(nums, func(n int) int { return n * 2 })
// [2, 4, 6]

strs := Map(nums, func(n int) string {
    return fmt.Sprintf("num%d", n)
})
// ["num1", "num2", "num3"]

// Type constraints
type Number interface {
    int | int64 | float64
}

func Sum[T Number](nums []T) T {
    var total T
    for _, n := range nums {
        total += n
    }
    return total
}

// comparable constraint (for maps/slices keys)
func Contains[T comparable](slice []T, target T) bool {
    for _, v := range slice {
        if v == target {
            return true
        }
    }
    return false
}

Методы

Методы — функции с аргументом-получателем. Value-получатели (r Rectangle) работают с копией — не могут модифицировать оригинал. Pointer-получатели (r *Rectangle) могут модифицировать и избегают копирования больших структур. Последовательность важна: если один метод использует pointer-получатель, все должны. Методы можно определять на любом типе в том же пакете.

go
type Rectangle struct {
    Width, Height float64
}

// Method with value receiver
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

// Method with pointer receiver (can modify)
func (r *Rectangle) Scale(factor float64) {
    r.Width *= factor
    r.Height *= factor
}

// Method on non-struct types
type MyString string

func (s MyString) Shout() string {
    return strings.ToUpper(string(s)) + "!"
}

// Usage
r := Rectangle{Width: 10, Height: 5}
fmt.Println(r.Area()) // 50
r.Scale(2)
fmt.Println(r.Width)  // 20

s := MyString("hello")
fmt.Println(s.Shout()) // HELLO!
05

Структуры данных

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

Массивы имеют фиксированную длину; срезы динамические (на базе массивов). append() добавляет элементы, увеличивая capacity по необходимости. make([]T, len, cap) предварительно выделяет для эффективности. Срез создаёт представление (разделяет базовый массив) — используйте copy() для независимых данных. Всегда проверяйте len/cap при оптимизации.

go
// Array (fixed length, rarely used directly)
var arr [3]int = [3]int{1, 2, 3}
arr2 := [...]int{4, 5, 6} // size inferred

// Slice (dynamic array, most common)
nums := []int{1, 2, 3}
nums = append(nums, 4)        // [1, 2, 3, 4]
nums = append(nums, 5, 6, 7) // [1, 2, 3, 4, 5, 6, 7]

// Make a slice with capacity
s := make([]int, 3, 10) // len=3, cap=10

// Slicing
sub := nums[1:4]  // [2, 3, 4]
first := nums[:2] // [1, 2]
last := nums[3:]  // [4, 5, 6, 7]

// Copy
dst := make([]int, len(nums))
copy(dst, nums)

// Length and capacity
fmt.Println(len(nums), cap(nums))

Мапы

Мапы — хеш-таблицы Go — неупорядоченные пары ключ/значение. Паттерн comma-ok (val, ok := m[key]) проверяет существование ключа. delete() удаляет ключ. Порядок итерации мапы случаен по дизайну. Мапы — ссылочные типы — передача в функции разделяет базовые данные. nil-мапы нельзя записывать (используйте make()).

go
// Create a map
m := map[string]int{
    "Alice": 30,
    "Bob":   25,
}

// Using make
ages := make(map[string]int)
ages["Charlie"] = 35

// Access
fmt.Println(m["Alice"]) // 30

// Check existence (comma-ok pattern)
age, ok := m["David"]
if !ok {
    fmt.Println("not found")
}

// Delete
delete(m, "Bob")

// Iterate (random order)
for name, age := range m {
    fmt.Printf("%s: %d\n", name, age)
}

// Length
fmt.Println(len(m))

// Nested maps
matrix := map[string]map[string]int{}
matrix["row1"] = map[string]int{"col1": 1}

Структуры

Структуры группируют связанные поля. Используйте именованную инициализацию (Person{Name: ...}) для ясности. new() возвращает указатель с нулевыми значениями. Анонимные структуры полезны для одноразовых форм данных. Встраивание структур (без имени поля) продвигает поля и методы встроенной структуры — альтернатива Go наследованию.

go
// Define a struct
type Person struct {
    Name string
    Age  int
    Address string // field names are exported (capitalized)
}

// Create instances
p1 := Person{Name: "Alice", Age: 30}
p2 := Person{"Bob", 25, "NYC"} // positional (not recommended)
p3 := new(Person)              // returns *Person, zero values
p3.Name = "Charlie"

// Anonymous struct (one-off)
config := struct {
    Port int
    Host string
}{
    Port: 8080,
    Host: "localhost",
}

// Struct embedding (composition)
type Employee struct {
    Person          // embedded (promotes fields)
    Salary   float64
}

emp := Employee{
    Person: Person{Name: "Dave", Age: 40},
    Salary: 50000,
}
fmt.Println(emp.Name) // "Dave" (promoted from Person)

Указатели

Указатели Go (*T) хранят адреса памяти. & берёт адрес, * разыменовывает. В отличие от C, в Go нет арифметики указателей (безопаснее). Указатели на структуры позволяют сокращение (u.Name вместо (*u).Name). nil-указатели вызывают панику при разыменовании. В Go есть сборка мусора — не нужен ручной free().

go
// Pointer basics
x := 42
p := &x          // p is *int, points to x
fmt.Println(*p)  // 42 (dereference)
*p = 100         // modify x through pointer
fmt.Println(x)   // 100

// new() allocates and returns pointer
p2 := new(int)   // *int, value 0
*p2 = 42

// Pointers to structs
type User struct{ Name string }
u := &User{Name: "Alice"} // *User
u.Name = "Bob"            // (*u).Name shorthand
fmt.Println(u.Name)       // Bob

// nil pointer
var p3 *int // nil
// *p3 // panic: nil pointer dereference

// No pointer arithmetic (unlike C)
// p++ is illegal

Операции со срезами

В Go нет встроенных filter/map/reduce — пишите их или используйте пакет slices (Go 1.21+). Insert/remove требуют сдвига элементов через append+copy. Будьте осторожны с алиасингом срезов: s[:i] и s[i+1:] разделяют базовый массив. Для 2D-срезов выделяйте каждую строку отдельно.

go
// Filter
func filter(nums []int, pred func(int) bool) []int {
    result := []int{}
    for _, n := range nums {
        if pred(n) {
            result = append(result, n)
        }
    }
    return result
}

evens := filter([]int{1,2,3,4,5}, func(n int) bool {
    return n%2 == 0
}) // [2, 4]

// Insert at index
func insert(s []int, i, v int) []int {
    s = append(s, 0)
    copy(s[i+1:], s[i:])
    s[i] = v
    return s
}

// Remove at index
func remove(s []int, i int) []int {
    return append(s[:i], s[i+1:]...)
}

// 2D slice
matrix := make([][]int, 3)
for i := range matrix {
    matrix[i] = make([]int, 4)
}

Сортировка и поиск

sort.Ints/Strings/Float64s сортируют на месте. sort.Slice с компаратором обрабатывает пользовательские типы. sort.Search* делает бинарный поиск по отсортированным срезам. Go 1.21+ добавляет пакет slices с generic-функциями сортировки. Вся сортировка in-place — сделайте копию сначала, если нужен исходный порядок.

go
import "sort"

// Sort a slice
nums := []int{3, 1, 4, 1, 5, 9, 2, 6}
sort.Ints(nums) // [1 1 2 3 4 5 6 9]
fmt.Println(nums)

// Sort strings
strs := []string{"banana", "apple", "cherry"}
sort.Strings(strs) // [apple banana cherry]

// Sort with custom comparator
type Person struct {
    Name string
    Age  int
}

people := []Person{
    {"Alice", 30},
    {"Bob", 25},
    {"Charlie", 35},
}

sort.Slice(people, func(i, j int) bool {
    return people[i].Age < people[j].Age // by age ascending
})

// Binary search (sorted slice)
idx := sort.SearchInts(nums, 4) // index of 4

// Sort package (Go 1.21+): slices.Sort
import "slices"
slices.Sort(people) // needs Less method or cmp func
06

Методы и интерфейсы

Определение интерфейсов

Интерфейсы определяют сигнатуры методов. Go использует структурную типизацию — тип реализует интерфейс автоматически, если имеет все требуемые методы (без явного объявления 'implements'). Это обеспечивает развязанный дизайн: определяйте интерфейсы там, где используете, а не там, где реализуете.

go
// Interface: a set of method signatures
type Shape interface {
    Area() float64
    Perimeter() float64
}

// Implement implicitly (no 'implements' keyword)
type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.Radius
}

// Use the interface
func describe(s Shape) {
    fmt.Printf("Area: %.2f, Perimeter: %.2f\n",
        s.Area(), s.Perimeter())
}

c := Circle{Radius: 5}
describe(c) // works! Circle implements Shape

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

interface{} (или 'any' в Go 1.18+) хранит любое значение. Type assertion (v.(T)) извлекает конкретный тип — паникует при неверном типе, поэтому используйте паттерн comma-ok. Type switch (switch v.(type)) чисто обрабатывает несколько типов. Пустой интерфейс полезен для generic-контейнеров, но теряет type safety — предпочитайте дженерики.

go
// Empty interface (any type)
// Go 1.18+: 'any' is an alias for interface{}
func printAny(v any) {
    fmt.Println(v)
}

printAny(42)
printAny("hello")
printAny([]int{1, 2, 3})

// Type assertion
var i any = "hello"
s := i.(string)    // panics if not string
fmt.Println(s)

// Safe type assertion (comma-ok)
s, ok := i.(string)
if ok {
    fmt.Println("string:", s)
}

// Type switch
func describe(v any) {
    switch x := v.(type) {
    case int:
        fmt.Printf("int: %d\n", x)
    case string:
        fmt.Printf("string: %s\n", x)
    case []int:
        fmt.Printf("int slice: %v\n", x)
    default:
        fmt.Printf("unknown type: %T\n", x)
    }
}

Композиция интерфейсов

Интерфейсы могут встраивать другие интерфейсы (композиция). io.Reader и io.Writer — самые важные интерфейсы Go — реализуются файлами, сетевыми подключениями, буферами и т.д. Это включает мощные абстракции: функции, принимающие io.Reader, работают с любым читаемым источником. Маленькие, сфокусированные интерфейсы (1-3 метода) идиоматичны.

go
// Compose interfaces
type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

// ReadWriter combines Reader and Writer
type ReadWriter interface {
    Reader
    Writer
}

// io.Reader and io.Writer are built-in interfaces
// Many types implement them: *os.File, *bytes.Buffer, net.Conn

func copyData(r io.Reader, w io.Writer) error {
    buf := make([]byte, 1024)
    for {
        n, err := r.Read(buf)
        if n > 0 {
            if _, err := w.Write(buf[:n]); err != nil {
                return err
            }
        }
        if err == io.EOF {
            break
        }
        if err != nil {
            return err
        }
    }
    return nil
}

Pointer vs Value Receivers

Pointer-получатели могут модифицировать структуру и избегать копирования больших значений. Value-получатели безопасны (не могут модифицировать) и допускают метод как на значениях, так и на указателях. Если любой метод имеет pointer-получатель, все должны (для согласованности). Методы pointer-получателя удовлетворяют интерфейсам для T и *T; методы value-получателя только для T.

go
type Counter struct {
    count int
}

// Value receiver: works on a copy, can't modify
func (c Counter) Get() int {
    return c.count
}

// Pointer receiver: can modify the original
func (c *Counter) Increment() {
    c.count++
}

c := Counter{}
c.Increment() // Go auto-takes address (&c).Increment()
c.Increment()
fmt.Println(c.Get()) // 2

// Interface implementation note:
// *Counter implements both Get() and Increment()
// Counter (value) implements only Get()
// So *Counter satisfies an interface requiring both

Интерфейс Stringer

Интерфейс Stringer (String() string) контролирует, как тип отображается в Print/Printf — как toString() в Java/JS. %v использует String(); %+v показывает имена полей; %#v показывает Go-синтаксис. Интерфейс error работает так же: реализуйте Error() string, чтобы сделать любой тип ошибкой. Это самые распространённые встроенные интерфейсы Go.

go
// Stringer interface (like toString() in other languages)
type Stringer interface {
    String() string
}

type Person struct {
    Name string
    Age  int
}

// Implement Stringer
func (p Person) String() string {
    return fmt.Sprintf("%s (%d years)", p.Name, p.Age)
}

p := Person{Name: "Alice", Age: 30}
fmt.Println(p)       // Alice (30 years) — uses String()
fmt.Printf("%v\n", p) // Alice (30 years)
fmt.Printf("%+v\n", p) // Name:Alice Age:30
fmt.Printf("%#v\n", p) // main.Person{Name:"Alice", Age:30}

// Error interface is similar:
// type error interface { Error() string }
// Implement Error() to make a type usable as an error

Встраивание типов и композиция

Go использует композицию вместо наследования. Встраивание структуры продвигает её поля и методы во внешнюю структуру. Можно переопределить продвигаемые методы, определив метод с тем же именем на внешнем типе. Встраивание интерфейса позволяет декорирование/делегирование — внешний тип удовлетворяет интерфейсу и может перенаправлять вызовы.

go
// Embed a struct (composition over inheritance)
type Animal struct {
    Name string
}

func (a Animal) Speak() string {
    return a.Name + " makes a sound"
}

type Dog struct {
    Animal // embedded — promotes Name and Speak()
    Breed  string
}

d := Dog{
    Animal: Animal{Name: "Rex"},
    Breed:  "Labrador",
}

fmt.Println(d.Name)      // "Rex" (promoted)
fmt.Println(d.Speak())   // "Rex makes a sound" (promoted)
fmt.Println(d.Animal.Name) // explicit access also works

// Override a promoted method
func (d Dog) Speak() string {
    return d.Name + " barks!"
}
fmt.Println(d.Speak()) // "Rex barks!"

// Embed an interface
type Logger struct {
    io.Writer // embed interface
}
// Logger now has Write() method, delegating to the embedded Writer
07

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

Основы ошибок

Go обрабатывает ошибки как значения, а не исключения. Функции возвращают (result, error) — всегда проверяйте err != nil немедленно. errors.New() создаёт простые ошибки; fmt.Errorf() добавляет форматирование. Sentinel-ошибки (var ErrX = errors.New()) разрешают сравнение через ==. Никогда не игнорируйте ошибки (используйте _ только намеренно).

go
// Functions return errors as last return value
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("cannot divide by zero")
    }
    return a / b, nil
}

// Always check errors immediately
result, err := divide(10, 0)
if err != nil {
    log.Fatal(err) // or handle gracefully
}
fmt.Println(result)

// fmt.Errorf for formatted errors
func validate(age int) error {
    if age < 0 {
        return fmt.Errorf("invalid age: %d (must be positive)", age)
    }
    return nil
}

// Sentinel errors
var ErrNotFound = errors.New("not found")
if err := lookup("key"); err == ErrNotFound {
    // handle not found
}

Пользовательские типы ошибок

Пользовательские типы ошибок реализуют интерфейс error (Error() string). Они несут структурированные данные (поля, коды, контекст) сверх простого сообщения. Используйте type assertion (*ValidationError) для доступа к данным. Это необходимо для domain-specific обработки ошибок — например, HTTP status codes, детали валидации, логика повторов.

go
// Custom error type (implements error interface)
type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation error on '%s': %s", e.Field, e.Message)
}

// Use the custom error
func validateUser(name string) error {
    if name == "" {
        return &ValidationError{
            Field:   "name",
            Message: "cannot be empty",
        }
    }
    return nil
}

// Type-assert to access fields
err := validateUser("")
if ve, ok := err.(*ValidationError); ok {
    fmt.Println("Field:", ve.Field)   // "name"
    fmt.Println("Message:", ve.Message) // "cannot be empty"
}

errors.Is и errors.As (Go 1.13+)

Go 1.13+ добавил обёртывание ошибок с %w (fmt.Errorf). errors.Is() проверяет, соответствует ли ошибка sentinel (разворачивая цепочку). errors.As() извлекает конкретный тип ошибки из цепочки. Это включает многоуровневую обработку ошибок: низкоуровневые ошибки обёртываются контекстом, проверяются на высоких уровнях. Всегда обёртывайте с %w (не %v) для сохранения цепочки.

go
import "errors"

// Wrapped errors with %w
var ErrNotFound = errors.New("not found")

func getUser(id int) error {
    if id == 0 {
        return fmt.Errorf("getUser(%d): %w", id, ErrNotFound)
    }
    return nil
}

// errors.Is: check if error matches (unwraps the chain)
err := getUser(0)
if errors.Is(err, ErrNotFound) {
    fmt.Println("user not found")
}

// errors.As: extract a specific error type
var ve *ValidationError
if errors.As(err, &ve) {
    fmt.Println("field:", ve.Field)
}

// errors.Unwrap: get the wrapped error
inner := errors.Unwrap(err)

Panic и Recover

panic() для невосстановимых ошибок (баги, нарушения инвариантов) — не для обычной обработки ошибок. recover() ловит паники, но работает только в отложенных функциях. Используйте panic/recover для: ошибок программирования (индекс вне диапазона), сбоев инициализации пакета и защиты горутин от краша программы. Предпочитайте возврат ошибок для ожидаемых сбоев.

go
// Panic: unrecoverable error (like throw)
func mustParse(s string) int {
    n, err := strconv.Atoi(s)
    if err != nil {
        panic(fmt.Sprintf("invalid number: %s", s))
    }
    return n
}

// Recover: catch a panic (only in deferred functions)
func safeCall() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("recovered:", r)
        }
    }()
    panic("something went wrong")
}

// Practical: safe goroutine
func safeGo(fn func()) {
    go func() {
        defer func() {
            if r := recover(); r != nil {
                log.Println("goroutine panic:", r)
            }
        }()
        fn()
    }()
}

Паттерны обёртывания ошибок

Оборачивайте ошибки с контекстом на каждом уровне через fmt.Errorf с %w. Это создаёт цепочку ошибок: верхний уровень видит полный путь (getUserProfile → fetchUser → sql error). Контекст (имя функции, параметры) помогает отладке. Используйте errors.Is/As для проверки конкретных ошибок в цепочке. Избегайте многократного обёртывания одной ошибки одним контекстом.

go
// Wrap errors with context as they propagate up
func fetchUser(id int) (*User, error) {
    row := db.QueryRow("SELECT ... WHERE id = ?", id)
    var u User
    if err := row.Scan(&u.Name, &u.Age); err != nil {
        return nil, fmt.Errorf("fetchUser(%d): %w", id, err)
    }
    return &u, nil
}

func getUserProfile(id int) (*Profile, error) {
    user, err := fetchUser(id)
    if err != nil {
        return nil, fmt.Errorf("getUserProfile(%d): %w", id, err)
    }
    // ...
}

// At the top level, log the full chain
profile, err := getUserProfile(42)
if err != nil {
    log.Printf("error: %v", err)
    // Output: getUserProfile(42): fetchUser(42): sql: no rows
}

// golang.org/x/xerrors or pkg/errors for stack traces

Defer, Panic, Recover вместе

Комбинируйте defer (очистка), panic (фатальные ошибки) и recover (ловля паник) для надёжного управления ресурсами. Defers выполняются в порядке LIFO, даже при панике. Именованное возвращаемое значение (err error) можно установить внутри отложенного recover. Этот паттерн гарантирует закрытие файлов/подключений и откат транзакций даже при панике кода.

go
// Complete pattern: cleanup + panic recovery
func processFile(path string) (err error) {
    file, e := os.Open(path)
    if e != nil {
        return e
    }
    defer func() {
        // Close file regardless of panic
        file.Close()

        // Recover from panic and convert to error
        if r := recover(); r != nil {
            err = fmt.Errorf("panic in processFile: %v", r)
        }
    }()

    // Do work that might panic
    data := parseFile(file) // might panic
    return saveData(data)
}

// Defer for resource cleanup (always runs)
func withDatabase(db *sql.DB, fn func(*sql.DB) error) error {
    tx, err := db.Begin()
    if err != nil {
        return err
    }
    defer func() {
        if err != nil {
            tx.Rollback()
        } else {
            tx.Commit()
        }
    }()
    return fn(tx)
}
08

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

Горутины

Горутины — лёгкие потоки Go — запускаются через 'go'. Они дешёвые (~2KB стека) и управляются планировщиком Go (M:N scheduling). Функция main не ждёт горутины — используйте sync.WaitGroup или каналы для синхронизации. Никогда не используйте time.Sleep для синхронизации в production (используйте WaitGroup).

go
package main

import (
    "fmt"
    "time"
)

func sayHello(name string) {
    for i := 0; i < 3; i++ {
        fmt.Println(name, i)
        time.Sleep(100 * time.Millisecond)
    }
}

func main() {
    // Launch goroutine with 'go' keyword
    go sayHello("Alice")
    go sayHello("Bob")

    // Anonymous goroutine
    go func() {
        fmt.Println("anonymous goroutine")
    }()

    // Wait for goroutines (simple approach)
    time.Sleep(1 * time.Second)
    fmt.Println("done")
}

// Goroutines are lightweight (~2KB stack, grows as needed)
// Millions of goroutines can run concurrently

Каналы

Каналы — типизированные каналы для общения горутин. Небуферизованные (make(chan T)) блокируют до готовности отправителя и получателя (синхронно). Буферизованные (make(chan T, n)) блокируют только при заполнении (асинхронно). Отправитель должен закрывать каналы, никогда получатель. Range по каналу до его закрытия. Каналы включают 'разделять память через общение'.

go
// Unbuffered channel (synchronous)
ch := make(chan string)

// Send and receive
go func() {
    ch <- "hello" // blocks until received
}()
msg := <-ch // blocks until sent
fmt.Println(msg)

// Buffered channel (asynchronous)
buf := make(chan int, 3)
buf <- 1 // doesn't block (buffer has space)
buf <- 2
buf <- 3
// buf <- 4 // would block (buffer full)
fmt.Println(<-buf) // 1

// Close a channel (sender closes, never receiver)
close(buf)

// Range over channel (until closed)
for v := range buf {
    fmt.Println(v)
}

// Check if closed
v, ok := <-buf
if !ok {
    fmt.Println("channel closed")
}

Оператор Select

select позволяет горутине ждать несколько операций канала — выбирает первую готовую (случайно при нескольких). default делает его неблокирующим. time.After() создаёт каналы таймаута. Select — сердце конкурентного Go: циклы событий, fan-in/fan-out, таймауты. Всегда включайте таймауты для избежания дедлоков.

go
// Select: multiplex channel operations
ch1 := make(chan string)
ch2 := make(chan string)

go func() {
    time.Sleep(1 * time.Second)
    ch1 <- "one"
}()
go func() {
    time.Sleep(2 * time.Second)
    ch2 <- "two"
}()

// Wait for first to arrive
for i := 0; i < 2; i++ {
    select {
    case msg1 := <-ch1:
        fmt.Println("received:", msg1)
    case msg2 := <-ch2:
        fmt.Println("received:", msg2)
    }
}

// Timeout with select
select {
case result := <-slowOperation():
    fmt.Println(result)
case <-time.After(3 * time.Second):
    fmt.Println("timeout!")
}

// Non-blocking receive (default case)
select {
case msg := <-ch:
    fmt.Println(msg)
default:
    fmt.Println("no message") // runs if no data
}

sync.WaitGroup

sync.WaitGroup ждёт группу горутин. Add(n) инкрементирует счётчик, Done() декрементирует (используйте defer), Wait() блокирует до нуля. Всегда передавайте переменные цикла как параметры горутинам для избежания багов захвата замыкания (исправлено в Go 1.22, но всё равно рекомендуется). WaitGroup проще каналов для fire-and-forget конкурентности.

go
import "sync"

func main() {
    var wg sync.WaitGroup

    // Launch 5 goroutines
    for i := 0; i < 5; i++ {
        wg.Add(1) // increment counter
        go func(id int) {
            defer wg.Done() // decrement when done
            fmt.Printf("Worker %d started\n", id)
            time.Sleep(time.Duration(id) * 100 * time.Millisecond)
            fmt.Printf("Worker %d done\n", id)
        }(i)
    }

    wg.Wait() // block until all Done() called
    fmt.Println("all workers finished")
}

// Common mistake: passing loop variable to goroutine
// Always pass as parameter: go func(i int) { ... }(i)
// (Go 1.22+ fixes loop variable scoping, but still good practice)

Mutex и Sync

sync.Mutex защищает общее состояние от конкурентного доступа — Lock/Unlock с defer. RWMutex разрешает несколько читателей или одного писателя (лучше для read-heavy нагрузок). sync.Once гарантирует инициализацию ровно один раз (паттерн singleton). Предпочитайте каналы для общения, mutex'ы для защиты общего состояния. 'Разделяйте память через общение; не общайтесь через разделение памяти.'

go
import "sync"

// Mutex: mutual exclusion lock
type SafeCounter struct {
    mu    sync.Mutex
    count int
}

func (c *SafeCounter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock() // always unlock with defer
    c.count++
}

func (c *SafeCounter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.count
}

// RWMutex: multiple readers, one writer
type Cache struct {
    mu   sync.RWMutex
    data map[string]string
}

func (c *Cache) Get(key string) (string, bool) {
    c.mu.RLock()         // read lock (multiple allowed)
    defer c.mu.RUnlock()
    val, ok := c.data[key]
    return val, ok
}

func (c *Cache) Set(key, val string) {
    c.mu.Lock()          // write lock (exclusive)
    defer c.mu.Unlock()
    c.data[key] = val
}

// sync.Once: run exactly once
var (
    once sync.Once
    instance *Database
)

func GetDB() *Database {
    once.Do(func() {
        instance = &Database{}
    })
    return instance
}

Паттерны конкурентности

Worker pool: фиксированное число горутин обрабатывает задачи из канала, отправляя результаты в другой. Это ограничивает конкурентность и предотвращает истощение ресурсов. Fan-out/fan-in: распределение работы по горутинам, затем слияние результатов. Ключевой инсайт: закрывайте каналы, когда больше не будет данных, чтобы range-циклы завершались. Эти паттерны — основа конкурентного Go.

go
// Worker pool
func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        time.Sleep(time.Second) // simulate work
        results <- j * 2
    }
}

func main() {
    jobs := make(chan int, 100)
    results := make(chan int, 100)

    // Start 3 workers
    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }

    // Send 5 jobs
    for j := 1; j <= 5; j++ {
        jobs <- j
    }
    close(jobs)

    // Collect results
    for a := 1; a <= 5; a++ {
        fmt.Println(<-results)
    }
}

// Fan-out/Fan-in
func fanOut(input <-chan int, workers int) <-chan int {
    output := make(chan int)
    var wg sync.WaitGroup
    wg.Add(workers)
    for i := 0; i < workers; i++ {
        go func() {
            defer wg.Done()
            for v := range input {
                output <- v * 2
            }
        }()
    }
    go func() { wg.Wait(); close(output) }()
    return output
}
09

Файловый I/O и OS

Чтение файлов

os.ReadFile() читает весь файл в память (просто, но не для больших файлов). Для больших файлов откройте через os.Open(), читайте чанками и всегда defer Close(). bufio.Scanner читает построчно — идеально для обработки текста. Всегда проверяйте ошибки, особенно io.EOF для определения завершения чтения.

go
import (
    "io"
    "os"
)

// Read entire file (small files)
data, err := os.ReadFile("input.txt")
if err != nil {
    log.Fatal(err)
}
fmt.Println(string(data))

// Open and read (large files)
file, err := os.Open("large.txt")
if err != nil {
    log.Fatal(err)
}
defer file.Close()

// Read in chunks
buf := make([]byte, 1024)
for {
    n, err := file.Read(buf)
    if n > 0 {
        // process buf[:n]
    }
    if err == io.EOF {
        break
    }
    if err != nil {
        log.Fatal(err)
    }
}

// Read line by line
scanner := bufio.NewScanner(file)
for scanner.Scan() {
    line := scanner.Text()
    fmt.Println(line)
}

Запись файлов

os.WriteFile() создаёт/усекает и пишет атомарно (просто). os.OpenFile() с флагами даёт контроль: O_APPEND (добавить в конец), O_CREATE (создать если нет), O_TRUNC (усечь). Используйте bufio.Writer для множества маленьких записей (буферизует в памяти, flush в конце). Файловый режим 0644: владелец может читать/писать, остальные читать.

go
// Write entire file (creates or truncates)
err := os.WriteFile("output.txt", []byte("Hello, World!"), 0644)
if err != nil {
    log.Fatal(err)
}

// Open for writing (with options)
file, err := os.OpenFile("log.txt",
    os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
    log.Fatal(err)
}
defer file.Close()

// Write
if _, err := file.WriteString("log entry\n"); err != nil {
    log.Fatal(err)
}

// Buffered writer (efficient for many small writes)
writer := bufio.NewWriter(file)
for i := 0; i < 1000; i++ {
    writer.WriteString(fmt.Sprintf("line %d\n", i))
}
writer.Flush() // don't forget to flush!

// File modes: 0644 = owner read/write, others read
// os.O_APPEND, O_CREATE, O_WRONLY, O_RDONLY, O_RDWR, O_TRUNC

Файловые операции

os.Mkdir/MkdirAll создают каталоги. os.ReadDir эффективно перечисляет записи. filepath.WalkDir рекурсивно обходит каталоги. os.Stat даёт информацию о файле (размер, время модификации, разрешения). os.IsNotExist проверяет отсутствие файла. os.Remove удаляет один файл; RemoveAll удаляет каталоги рекурсивно. Всегда проверяйте ошибки.

go
// Create directory
os.Mkdir("mydir", 0755)       // one level
os.MkdirAll("a/b/c", 0755)    // nested directories

// List directory
entries, err := os.ReadDir(".")
if err != nil {
    log.Fatal(err)
}
for _, entry := range entries {
    fmt.Println(entry.Name(), entry.IsDir())
}

// Walk directory tree
filepath.WalkDir(".", func(path string, d fs.DirEntry, err error) error {
    if err != nil {
        return err
    }
    fmt.Println(path)
    return nil
})

// File info
info, _ := os.Stat("file.txt")
fmt.Println(info.Size())      // bytes
fmt.Println(info.ModTime())   // last modified
fmt.Println(info.IsDir())

// Check existence
if _, err := os.Stat("file.txt"); os.IsNotExist(err) {
    fmt.Println("file does not exist")
}

// Remove
os.Remove("file.txt")
os.RemoveAll("directory") // recursive

Окружение и командная строка

os.Getenv/LookupEnv/Setenv управляют переменными окружения. os.Args даёт сырые аргументы командной строки. Пакет flag предоставляет распарсенные флаги со значениями по умолчанию и help-текстом. LookupEnv различает unset и пустые значения. Переменные окружения — стандартный способ конфигурации 12-factor приложений (API-ключи, URL баз данных).

go
// Environment variables
os.Setenv("API_KEY", "secret")
key := os.Getenv("API_KEY") // "" if not set

// With existence check
if val, ok := os.LookupEnv("HOME"); ok {
    fmt.Println("HOME:", val)
}

// All environment variables
for _, e := range os.Environ() {
    fmt.Println(e)
}

// Command line arguments
// args := os.Args // []string, args[0] is program name

// Using flag package
var port int
var host string
flag.IntVar(&port, "port", 8080, "server port")
flag.StringVar(&host, "host", "localhost", "server host")
flag.Parse()
fmt.Printf("Running on %s:%d\n", host, port)

// Run: go run main.go -port 3000 -host 0.0.0.0

Выполнение команд

os/exec запускает внешние команды. Command() создаёт команду; Output() захватывает stdout; Run() выполняет с кастомным Stdout/Stderr. Используйте CommandContext для таймаутов (убивает процесс). Всегда проверяйте ошибки — exec.ExitError указывает ненулевые коды выхода. Будьте осторожны с пользовательским вводом для предотвращения инъекции команд.

go
import "os/exec"

// Run a command and get output
cmd := exec.Command("ls", "-la", "/tmp")
output, err := cmd.Output()
if err != nil {
    log.Fatal(err)
}
fmt.Println(string(output))

// Capture stdout and stderr separately
cmd = exec.Command("git", "status")
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err = cmd.Run()
fmt.Println("stdout:", stdout.String())
fmt.Println("stderr:", stderr.String())

// Pipe input to command
cmd = exec.Command("grep", "error")
cmd.Stdin = strings.NewReader("info\nerror\nwarn\n")
result, _ := cmd.Output()
fmt.Println(string(result)) // "error"

// Run with context (timeout)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cmd = exec.CommandContext(ctx, "sleep", "10")
err = cmd.Run() // killed after 5s
10

JSON и кодирование

JSON Marshal и Unmarshal

encoding/json преобразует между структурами Go и JSON. Marshal (структура → JSON), Unmarshal (JSON → структура). Теги структуры (json:"name") контролируют именование полей и видимость. omitempty пропускает нулевые/пустые значения. json:"-" полностью исключает поле. Это стандартный способ обработки API-запросов/ответов в Go.

go
import "encoding/json"

type User struct {
    ID       int    `json:"id"`
    Name     string `json:"name"`
    Email    string `json:"email,omitempty"` // omit if empty
    Password string `json:"-"`               // never serialize
    Age      int    `json:"age,omitempty"`
}

// Struct to JSON (marshal)
user := User{ID: 1, Name: "Alice", Age: 30}
data, err := json.Marshal(user)
// {"id":1,"name":"Alice","age":30}

// Pretty print
pretty, _ := json.MarshalIndent(user, "", "  ")

// JSON to struct (unmarshal)
jsonStr := `{"id":2,"name":"Bob","email":"[email protected]"}`
var u User
err := json.Unmarshal([]byte(jsonStr), &u)
fmt.Println(u.Name) // "Bob"

// Struct tags control field names:
// json:"name"        -> rename
// json:"name,omitempty" -> skip if zero value
// json:"-"           -> skip entirely
// json:",omitempty"  -> keep name, skip if empty

JSON с мапами и срезами

Для динамического JSON (неизвестная структура) unmarshal в map[string]any (или interface{}). JSON-числа становятся float64 — type-assert для доступа. json.Decoder/Encoder работают с потоками (файлы, HTTP-тела) эффективно. Используйте struct unmarshaling, когда знаете схему; используйте мапы для гибких/динамических данных.

go
// Parse arbitrary JSON into map[string]interface{}
jsonStr := `{"name":"Alice","scores":[90,85,92],"active":true}`

var result map[string]any
json.Unmarshal([]byte(jsonStr), &result)

// Type-assert to access values
name := result["name"].(string)
scores := result["scores"].([]any)
for _, s := range scores {
    fmt.Println(s.(float64)) // JSON numbers are float64
}

// Encode a map
m := map[string]any{
    "count": 42,
    "items": []string{"a", "b", "c"},
    "meta":  map[string]any{"page": 1},
}
data, _ := json.Marshal(m)

// Decode JSON stream
dec := json.NewDecoder(strings.NewReader(jsonStr))
var v map[string]any
dec.Decode(&v)

// Encode to stream
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", "  ")
enc.Encode(user)

Пользовательский JSON Marshaling

Реализуйте MarshalJSON/UnmarshalJSON для пользовательской сериализации. Полезно для: вычисляемых полей, альтернативных форматов (Money как '99.99 USD'), обработки чувствительных данных и форматирования времени. Method receiver для UnmarshalJSON должен быть указателем для модификации структуры. time.Time автоматически сериализуется как RFC 3339 строки.

go
// Implement MarshalJSON/UnmarshalJSON
type Money struct {
    Amount   float64
    Currency string
}

func (m Money) MarshalJSON() ([]byte, error) {
    return json.Marshal(map[string]any{
        "amount":   m.Amount,
        "currency": m.Currency,
        "display":  fmt.Sprintf("%.2f %s", m.Amount, m.Currency),
    })
}

func (m *Money) UnmarshalJSON(data []byte) error {
    var v struct {
        Amount   float64
        Currency string
    }
    if err := json.Unmarshal(data, &v); err != nil {
        return err
    }
    m.Amount = v.Amount
    m.Currency = v.Currency
    return nil
}

money := Money{Amount: 99.99, Currency: "USD"}
data, _ := json.Marshal(money)
// {"amount":99.99,"currency":"USD","display":"99.99 USD"}

// Time formatting
type Event struct {
    Time time.Time `json:"time"`
}
// time.Time marshals as RFC 3339 by default

HTTP-сервер

Пакет net/http строит HTTP-серверы. http.HandleFunc регистрирует обработчики. http.ResponseWriter пишет ответ; *http.Request читает запрос. Для JSON API установите Content-Type и используйте json.NewEncoder(w).Encode(data). Стандартная библиотека готова для production — фреймворк не нужен для простых API. Используйте mux-роутеры (gorilla/mux, chi) для сложной маршрутизации.

go
import (
    "encoding/json"
    "net/http"
)

type Response struct {
    Status  string `json:"status"`
    Message string `json:"message"`
}

func main() {
    // Simple handler
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello, %s!", r.URL.Path[1:])
    })

    // JSON API endpoint
    http.HandleFunc("/api/user", func(w http.ResponseWriter, r *http.Request) {
        user := User{ID: 1, Name: "Alice"}
        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(user)
    })

    // Start server
    fmt.Println("Server running on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

// Request methods:
// r.Method  -> "GET", "POST", etc.
// r.URL.Query().Get("key") -> query params
// r.Body    -> request body (io.ReadCloser)

HTTP-клиент

http.Get/Post — удобные ярлыки. Для кастомных заголовков, методов или тел используйте http.NewRequest + client.Do(). Всегда defer resp.Body.Close() для избежания утечек подключений. Установите таймаут на клиенте (по умолчанию без таймаута — опасно). Для production переиспользуйте http.Client (он connection-pools) и используйте context для отмены.

go
// Simple GET
resp, err := http.Get("https://api.example.com/users")
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
fmt.Println(string(body))

// Custom request with headers
req, err := http.NewRequest("GET", "https://api.example.com/data", nil)
req.Header.Set("Authorization", "Bearer token123")
req.Header.Set("Accept", "application/json")

resp, err := http.DefaultClient.Do(req)
defer resp.Body.Close()

// POST with JSON body
user := User{Name: "Alice"}
body, _ := json.Marshal(user)
resp, err := http.Post(
    "https://api.example.com/users",
    "application/json",
    bytes.NewBuffer(body),
)
defer resp.Body.Close()

// With timeout
client := &http.Client{Timeout: 10 * time.Second}
resp, err = client.Get("https://slow-api.example.com")
11

Тестирование и бенчмаркинг

Unit-тесты

Тестовые файлы заканчиваются на _test.go, тестовые функции начинаются с Test. Table-driven tests — идиоматичный паттерн: определите тестовые случаи в срезе, цикл с t.Run для подтестов (именованные, индивидуально запускаемые). Используйте t.Errorf для ошибок (продолжает), t.Fatalf для фатальных (останавливает). Запускайте через 'go test -v' для подробного вывода.

go
// math_test.go (file must end with _test.go)
package math

import "testing"

// Test function: func TestXxx(t *testing.T)
func TestAdd(t *testing.T) {
    result := Add(2, 3)
    expected := 5
    if result != expected {
        t.Errorf("Add(2, 3) = %d; want %d", result, expected)
    }
}

// Table-driven tests (idiomatic Go)
func TestAddTable(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"positive", 2, 3, 5},
        {"negative", -1, -1, -2},
        {"zero", 0, 0, 0},
        {"mixed", -5, 10, 5},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := Add(tt.a, tt.b)
            if result != tt.expected {
                t.Errorf("Add(%d, %d) = %d; want %d",
                    tt.a, tt.b, result, tt.expected)
            }
        })
    }
}

// Run: go test -v
// Run specific: go test -run TestAddTable/positive

Бенчмарки

Бенчмарк-функции начинаются с Benchmark и используют *testing.B. Цикл b.N настраивается runtime для стабильных измерений. Запускайте через 'go test -bench=.'. b.ResetTimer() исключает время настройки. b.ReportAllocs() показывает выделения памяти. Сравнивайте реализации через 'benchstat' для верификации значимых улучшений.

go
// Benchmark function: func BenchmarkXxx(b *testing.B)
func BenchmarkFibonacci(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Fibonacci(20)
    }
}

// Benchmark with allocations
func BenchmarkStringConcat(b *testing.B) {
    for i := 0; i < b.N; i++ {
        s := ""
        for j := 0; j < 100; j++ {
            s += "a"
        }
    }
}

// Run: go test -bench=.
// Output:
// BenchmarkFibonacci-8        300     4234567 ns/op
// BenchmarkStringConcat-8     200     6789012 ns/op   100 B/op   1 allocs/op

// Benchmark with setup
func BenchmarkProcess(b *testing.B) {
    data := setup() // setup not measured
    b.ResetTimer()  // start timing here
    for i := 0; i < b.N; i++ {
        process(data)
    }
}

// Memory allocations
func BenchmarkAllocs(b *testing.B) {
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
        make([]int, 100)
    }
}

Тестовые помощники и mocking

t.Helper() улучшает сообщения об ошибках, пропуская helper-функции в стек-трейсах. Mock через реализацию интерфейсов (подход Go к mocking — без mock-фреймворка). t.Cleanup() регистрирует функции очистки (как defer, но для scope теста). Для сложного mocking используйте testify/assert и mockery или gomock для генерации mock из интерфейсов.

go
// t.Helper() marks helper functions (better error locations)
func assertEqual(t *testing.T, got, want int) {
    t.Helper()
    if got != want {
        t.Errorf("got %d, want %d", got, want)
    }
}

// Interfaces for mocking
type DataStore interface {
    Get(key string) (string, error)
}

type MockStore struct {
    data map[string]string
}

func (m *MockStore) Get(key string) (string, error) {
    if v, ok := m.data[key]; ok {
        return v, nil
    }
    return "", errors.New("not found")
}

func TestService(t *testing.T) {
    store := &MockStore{data: map[string]string{"key": "value"}}
    svc := NewService(store)
    result := svc.GetValue("key")
    assertEqual(t, result, "value")
}

// Subtests with t.Run for setup/teardown
func TestWithCleanup(t *testing.T) {
    t.Cleanup(func() {
        // runs after test (LIFO order)
        os.Remove("tempfile")
    })
    // test code
}

Основные модули стандартной библиотеки

Стандартная библиотека Go всеобъемлющая. time обрабатывает даты/время (внимание: формат использует reference time 2006-01-02). regexp для сопоставления шаблонов. context для отмены/таймаутов между горутинами. sync.Pool для переиспользования объектов (снижает давление GC). Go 1.21+ добавляет пакеты slices и maps с generic-утилитами.

go
// time package
now := time.Now()
future := now.Add(24 * time.Hour)
formatted := now.Format("2006-01-02 15:04:05")
parsed, _ := time.Parse("2006-01-02", "2024-01-15")

// regexp
re := regexp.MustCompile(`\d{4}-\d{2}-\d{2}`)
matches := re.FindString("date: 2024-01-15")
all := re.FindAllString("2024-01-15 and 2024-02-20", -1)

// context (cancellation, timeouts)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
go doWork(ctx)
// cancel() or timeout stops doWork

// sync.Pool (reuse objects)
var bufPool = sync.Pool{
    New: func() any { return new(bytes.Buffer) },
}
buf := bufPool.Get().(*bytes.Buffer)
defer bufPool.Put(buf)

// sort (Go 1.21+ slices package)
slices.Sort(nums)
slices.Contains(nums, 42)
slices.Reverse(nums)
12

Горутины (углублённо)

Запуск горутин и WaitGroup

Горутины — лёгкие потоки, управляемые Go runtime (не OS-потоки) — можно породить сотни тысяч. sync.WaitGroup координирует завершение горутин: Add(1) перед запуском, Done() по завершении (используйте defer), Wait() для блокировки до нуля. Всегда передавайте указатель на WaitGroup, чтобы все горутины разделяли один счётчик.

go
package main

import (
    "fmt"
    "sync"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done() // signal completion when function returns
    fmt.Printf("Worker %d started\n", id)
    // ... do work ...
    fmt.Printf("Worker %d done\n", id)
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 5; i++ {
        wg.Add(1) // increment counter before starting goroutine
        go worker(i, &wg)
    }

    wg.Wait() // block until all goroutines call Done()
    fmt.Println("All workers finished")
}

GOMAXPROCS и планирование

GOMAXPROCS контролирует, сколько OS-потоков одновременно выполняют горутины — по умолчанию равно числу CPU-ядер и почти всегда оптимально. Go использует M:N scheduling: много горутин на нескольких OS-потоках. Блокирующие I/O или операции канала заставляют планировщик запускать другие горутины на том же потоке. runtime.Gosched() явно уступает. GOMAXPROCS редко нужно настраивать.

go
package main

import (
    "fmt"
    "runtime"
)

func main() {
    // GOMAXPROCS: number of OS threads available to run goroutines
    fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0)) // defaults to CPU cores

    // Set it manually (rarely needed — default is optimal)
    runtime.GOMAXPROCS(2)

    // Goroutines are multiplexed onto OS threads (M:N scheduling)
    // - Blocking syscalls (I/O) don't block other goroutines
    // - The scheduler steals work across threads
    // - A goroutine blocked on channel/IO yields its thread

    // Number of goroutines currently running
    go func() { runtime.Gosched() }() // yield to other goroutines
    fmt.Println("Goroutines:", runtime.NumGoroutine())
}

Утечки горутин и предотвращение

Утечка горутины происходит, когда горутина блокируется навсегда (например, отправка в небуферизованный канал, который никто не читает) — она никогда не собирается сборщиком мусора. Предотвращайте утечки: буферизация каналов, select с cancellation-каналом/context, всегда предоставляйте путь выхода. Утечки накапливают память и CPU. Используйте runtime.NumGoroutine() и pprof для обнаружения утечек в production.

go
package main

// BAD: goroutine leaks — this goroutine never exits
func leaky() <-chan int {
    ch := make(chan int)
    go func() {
        ch <- 42 // blocks forever if nobody reads from ch
    }()
    return ch
}

// GOOD: use context for cancellation
func safe(ctx context.Context) <-chan int {
    ch := make(chan int, 1) // buffered — sender never blocks
    go func() {
        select {
        case ch <- 42:
        case <-ctx.Done(): // exit when context is cancelled
            return
        }
    }()
    return ch
}

// GOOD: always ensure senders can exit
// 1. Buffer the channel so send doesn't block
// 2. Use select with a done/cancel channel
// 3. Use context.WithCancel/WithTimeout for lifecycle control

sync.Once (одноразовая инициализация)

sync.Once гарантирует, что функция выполняется ровно один раз во всех горутинах — стандартный способ реализации thread-safe singleton и ленивой инициализации. Эффективнее, чем проверка флага через mutex. Метод Do блокирует конкурентных вызывающих до завершения первого вызова. sync.Once также используется внутри многих паттернов стандартной библиотеки вроде sync.OnceValue (Go 1.21+).

go
package main

import (
    "fmt"
    "sync"
)

var (
    instance *Database
    once     sync.Once
)

type Database struct{ name string }

func GetDB() *Database {
    // sync.Once guarantees the function runs exactly once,
    // even if called from many goroutines simultaneously
    once.Do(func() {
        instance = &Database{name: "production"}
        fmt.Println("Database initialized")
    })
    return instance
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            GetDB() // "Database initialized" prints only once
        }()
    }
    wg.Wait()
}

Worker Pool с горутинами

Паттерн worker pool ограничивает конкурентность фиксированным числом горутин, обрабатывающих задачи из канала. Это предотвращает истощение ресурсов от порождения неограниченных горутин. Workers используют 'for job := range jobs', что выходит при закрытии канала. close(jobs) сигнализирует всем workers остановиться. Это канонический Go-паттерн конкурентности для ограниченного параллелизма.

go
package main

import (
    "fmt"
    "sync"
)

func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    for job := range jobs { // exits when jobs channel is closed
        results <- job * job // process and send result
    }
}

func main() {
    jobs := make(chan int, 100)
    results := make(chan int, 100)
    var wg sync.WaitGroup

    // Start 3 workers
    for w := 1; w <= 3; w++ {
        wg.Add(1)
        go worker(w, jobs, results, &wg)
    }

    // Send 5 jobs
    for j := 1; j <= 5; j++ {
        jobs <- j
    }
    close(jobs) // signal workers to stop (range loop exits)

    wg.Wait()
    close(results)

    for r := range results {
        fmt.Println("Result:", r)
    }
}
13

Каналы и Select

Основы каналов (небуферизованные и буферизованные)

Небуферизованные каналы (make(chan T)) синхронизируют отправителя и получателя — отправка блокирует до готовности получателя. Буферизованные каналы (make(chan T, n)) разрешают n отправок без получателя, разъединяя тайминги отправителя/получателя. Только отправитель должен закрывать канал (сигнал 'больше нет значений'). Получение из закрытого канала возвращает нулевое значение с ok=false. Range-циклы выходят при закрытии канала.

go
package main

import "fmt"

func main() {
    // Unbuffered: send blocks until someone receives (synchronous)
    ch := make(chan int)
    go func() {
        ch <- 42 // blocks until main reads
    }()
    fmt.Println(<-ch) // 42

    // Buffered: send blocks only when buffer is full
    buf := make(chan int, 3)
    buf <- 1 // doesn't block (buffer has space)
    buf <- 2
    buf <- 3
    // buf <- 4 // would block — buffer full
    fmt.Println(<-buf) // 1 (FIFO)

    // Close a channel (sender closes, never the receiver)
    close(buf)

    // Check if closed (ok is false when closed and empty)
    val, ok := <-buf
    fmt.Println(val, ok) // 2 true

    // Range over channel until closed
    nums := make(chan int, 3)
    nums <- 10; nums <- 20; nums <- 30
    close(nums)
    for n := range nums {
        fmt.Println(n) // 10, 20, 30
    }
}

Оператор Select

select позволяет горутине ждать несколько операций канала одновременно — выбирает первый готовый case (случайно при нескольких готовых). default делает select неблокирующим. time.After создаёт канал таймаута. select — сердце координации конкурентности Go: multiplexing, таймауты, отмена и fan-in/fan-out паттерны строятся на нём.

go
package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(1 * time.Second)
        ch1 <- "from ch1"
    }()
    go func() {
        time.Sleep(2 * time.Second)
        ch2 <- "from ch2"
    }()

    // select waits on multiple channel operations
    // picks the first one that's ready (random if multiple ready)
    for i := 0; i < 2; i++ {
        select {
        case msg := <-ch1:
            fmt.Println(msg)
        case msg := <-ch2:
            fmt.Println(msg)
        }
    }

    // Non-blocking receive with default
    select {
    case msg := <-ch1:
        fmt.Println(msg)
    default:
        fmt.Println("no message ready") // runs immediately
    }

    // Timeout with time.After
    select {
    case msg := <-ch1:
        fmt.Println(msg)
    case <-time.After(500 * time.Millisecond):
        fmt.Println("timed out")
    }
}

Паттерны Fan-In и Fan-Out

Fan-out распределяет работу по нескольким горутинам для параллелизма; fan-in сливает несколько каналов в один. Вместе они образуют параллельный конвейер: fan-out workers обрабатывают независимо, затем fan-in собирает результаты. Fan-in использует WaitGroup для закрытия объединённого канала только после исчерпания всех входных каналов. Эти паттерны фундаментальны для конкурентной обработки данных в Go.

go
package main

import (
    "fmt"
    "sync"
)

// Fan-Out: distribute work across multiple goroutines
func fanOut(input <-chan int, n int) []<-chan int {
    outputs := make([]<-chan int, n)
    for i := 0; i < n; i++ {
        outputs[i] = process(input) // each worker reads from same input
    }
    return outputs
}

func process(input <-chan int) <-chan int {
    output := make(chan int)
    go func() {
        defer close(output)
        for v := range input {
            output <- v * v // transform
        }
    }()
    return output
}

// Fan-In: merge multiple channels into one
func fanIn(channels ...<-chan int) <-chan int {
    var wg sync.WaitGroup
    merged := make(chan int)

    output := func(c <-chan int) {
        defer wg.Done()
        for v := range c {
            merged <- v
        }
    }

    wg.Add(len(channels))
    for _, c := range channels {
        go output(c)
    }

    go func() {
        wg.Wait()
        close(merged)
    }()
    return merged
}

Направленные каналы (только отправка/приём)

Направленные типы каналов (<-chan T только приём, chan<- T только отправка) обеспечивают использование канала на этапе компиляции. Двунаправленный канал неявно преобразуется в направленный при передаче в функцию. Это документирует намерение и предотвращает баги — функция-продюсер буквально не может получать из собственного выходного канала. Используйте направленные типы в сигнатурах функций для ясности контрактов.

go
package main

import "fmt"

// <-chan int: receive-only (can read, cannot send)
// chan<- int: send-only (can send, cannot read)
func producer(out chan<- int) { // can only send
    for i := 0; i < 3; i++ {
        out <- i
    }
    close(out)
}

func consumer(in <-chan int) { // can only receive
    for v := range in {
        fmt.Println("consumed:", v)
    }
}

func main() {
    ch := make(chan int)

    // Directional restrictions are enforced at compile time
    go producer(ch) // bidirectional chan converts to send-only
    consumer(ch)    // bidirectional chan converts to receive-only

    // This prevents bugs: producer can't accidentally read,
    // consumer can't accidentally send or close
}

Каналы Ticker и Timer

time.Ticker срабатывает повторно через интервалы — используйте ticker.C как канал в select для периодических задач. time.Timer срабатывает один раз через длительность. Всегда вызывайте Stop() на tickers и timers для освобождения ресурсов и избежания утечек. time.After — удобный одноразовый таймер, возвращающий канал (но не отменяется, поэтому предпочитайте NewTimer в select-циклах для избежания накопления). Reset позволяет перенести таймер.

go
package main

import (
    "fmt"
    "time"
)

func main() {
    // Ticker: fires repeatedly at intervals
    ticker := time.NewTicker(500 * time.Millisecond)
    defer ticker.Stop() // always stop to release resources

    go func() {
        for t := range ticker.C { // ticker.C is a channel
            fmt.Println("Tick at", t)
        }
    }()

    // Timer: fires once after a duration
    timer := time.NewTimer(2 * time.Second)
    <-timer.C // blocks until timer fires
    fmt.Println("Timer fired!")

    // time.After: one-shot timer as a channel (no Stop needed)
    select {
    case <-time.After(1 * time.Second):
        fmt.Println("1 second elapsed")
    }

    // Reset a timer (cancel and reschedule)
    timer2 := time.NewTimer(5 * time.Second)
    timer2.Reset(100 * time.Millisecond)
    <-timer2.C
}
14

Пакет Context

context.WithCancel и WithTimeout

context.Context переносит отмену, таймауты и request-scoped значения через границы горутин. WithCancel возвращает context и функцию cancel; WithTimeout автоматически отменяет через длительность. Горутины проверяют ctx.Done() (канал) в select для остановки. ВСЕГДА вызывайте функцию cancel (используйте defer) для освобождения ресурсов, даже если таймаут сработал — иначе context утекает.

go
package main

import (
    "context"
    "fmt"
    "time"
)

func worker(ctx context.Context, id int) {
    for {
        select {
        case <-ctx.Done(): // cancelled or timed out
            fmt.Printf("Worker %d stopped: %v\n", id, ctx.Err())
            return
        default:
            fmt.Printf("Worker %d working...\n", id)
            time.Sleep(500 * time.Millisecond)
        }
    }
}

func main() {
    // WithCancel: manual cancellation
    ctx, cancel := context.WithCancel(context.Background())
    go worker(ctx, 1)
    time.Sleep(2 * time.Second)
    cancel() // stop the worker
    time.Sleep(500 * time.Millisecond)

    // WithTimeout: auto-cancel after duration
    ctx2, cancel2 := context.WithTimeout(context.Background(), 1500*time.Millisecond)
    defer cancel2() // always call cancel to release resources
    go worker(ctx2, 2)
    time.Sleep(2 * time.Second) // worker stops after 1.5s
}

Распространение Context через вызовы

Context должен быть первым параметром каждой функции, делающей I/O, и должен распространяться через всю цепочку вызовов. http.Request.Context() автоматически отменяется при отключении клиента. Передача ctx в database/HTTP-операции (QueryRowContext, NewRequestWithContext) гарантирует их прерывание при отмене context — предотвращая лишнюю работу и утечки ресурсов. Никогда не храните context в структурах.

go
package main

import (
    "context"
    "database/sql"
    "net/http"
)

// Context should be the FIRST parameter, named ctx
func GetUser(ctx context.Context, db *sql.DB, id int) (string, error) {
    // Pass ctx to all blocking operations so they cancel together
    var name string
    err := db.QueryRowContext(ctx, "SELECT name FROM users WHERE id=?", id).Scan(&name)
    return name, err
}

func handler(w http.ResponseWriter, r *http.Request) {
    // r.Context() is cancelled when the client disconnects
    ctx := r.Context()

    name, err := GetUser(ctx, db, 42)
    if err != nil {
        http.Error(w, err.Error(), 500)
        return
    }
    w.Write([]byte(name))
}

// BEST PRACTICE: pass context through every function in the call chain
// that might do I/O. This ensures a client disconnect or timeout
// cancels ALL in-flight work (DB queries, HTTP calls, etc.)

Значения Context (request-scoped данные)

context.WithValue хранит request-scoped данные (user ID, trace ID, auth-токены), которые проходят через цепочку вызовов. Используйте кастомный тип ключа (не строку) для избежания коллизий. Значения должны быть данными, нужными запросу, а не параметрами функции — команда Go рекомендует использовать редко, в основном для cross-cutting concerns вроде tracing/auth. Всегда type-assert при получении значений.

go
package main

import (
    "context"
    "fmt"
    "net/http"
)

// Define a custom key type to avoid collisions
type contextKey string

const userIDKey contextKey = "userID"

// Set a value in context
func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        userID := 42 // authenticate...
        // WithValue creates a new context with the value
        ctx := context.WithValue(r.Context(), userIDKey, userID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// Get a value from context (must type-assert)
func getUserID(ctx context.Context) (int, bool) {
    if v, ok := ctx.Value(userIDKey).(int); ok {
        return v, true
    }
    return 0, false
}

func handler(w http.ResponseWriter, r *http.Request) {
    if uid, ok := getUserID(r.Context()); ok {
        fmt.Fprintf(w, "User ID: %d", uid)
    }
}

Graceful Shutdown с Context

Graceful shutdown позволяет текущим запросам завершиться до выхода сервера. signal.Notify захватывает OS-сигналы (Ctrl+C, SIGTERM от оркестраторов контейнеров). server.Shutdown(ctx) перестаёт принимать новые подключения и ждёт завершения активных (до таймаута context). Это необходимо для production-серверов — без него активные запросы резко прерываются, вызывая ошибки и повреждение данных.

go
package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    server := &http.Server{Addr: ":8080", Handler: mux}

    // Listen for OS interrupt signals (Ctrl+C, SIGTERM)
    stop := make(chan os.Signal, 1)
    signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)

    go func() {
        log.Println("Server starting on :8080")
        if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("Server error: %v", err)
        }
    }()

    <-stop // block until signal received
    log.Println("Shutting down...")

    // Give in-flight requests 10 seconds to finish
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    if err := server.Shutdown(ctx); err != nil {
        log.Printf("Forced shutdown: %v", err)
    }
    log.Println("Server stopped gracefully")
}

Context Deadline и обработка ошибок

context.WithDeadline отменяет в абсолютное время; WithTimeout отменяет через относительную длительность (WithTimeout — это просто WithDeadline(now+timeout)). ctx.Err() возвращает context.DeadlineExceeded или context.Canceled, чтобы различить причину. Используйте errors.Is() для проверки context-ошибок. Всегда проверяйте ctx.Err() в начале длинных операций и используйте select с ctx.Done() при блокирующих ожиданиях.

go
package main

import (
    "context"
    "errors"
    "fmt"
    "time"
)

func slowOperation(ctx context.Context) (string, error) {
    // Check if already cancelled before starting
    if err := ctx.Err(); err != nil {
        return "", err
    }

    select {
    case <-time.After(3 * time.Second): // simulate slow work
        return "result", nil
    case <-ctx.Done(): // cancelled or deadline exceeded
        return "", ctx.Err()
    }
}

func main() {
    // Deadline: cancel at a specific time
    deadline := time.Now().Add(1 * time.Second)
    ctx, cancel := context.WithDeadline(context.Background(), deadline)
    defer cancel()

    result, err := slowOperation(ctx)
    if err != nil {
        if errors.Is(err, context.DeadlineExceeded) {
            fmt.Println("Operation timed out")
        } else if errors.Is(err, context.Canceled) {
            fmt.Println("Operation was cancelled")
        }
        return
    }
    fmt.Println(result)
}
15

HTTP-сервер и клиент

HTTP-сервер (net/http)

net/http предоставляет production-готовый HTTP-сервер. http.HandleFunc регистрирует обработчики по пути. Используйте json.NewEncoder(w).Encode() для записи JSON-ответов и json.NewDecoder(r.Body).Decode() для парсинга тел запросов. Всегда устанавливайте Content-Type заголовки и проверяйте r.Method. http.Error отправляет error-статус. ListenAndServe запускает сервер; оберните с log.Fatal для ловли ошибок.

go
package main

import (
    "encoding/json"
    "log"
    "net/http"
)

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

func getUser(w http.ResponseWriter, r *http.Request) {
    user := User{ID: 1, Name: "Alice"}

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(user)
}

func createUser(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }

    var user User
    if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
        http.Error(w, "Invalid JSON", http.StatusBadRequest)
        return
    }
    user.ID = 99
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(user)
}

func main() {
    http.HandleFunc("/users/1", getUser)
    http.HandleFunc("/users", createUser)

    log.Println("Server on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

HTTP-клиент и запросы

Стандартный http.Client без таймаута — всегда устанавливайте для избежания зависания на неотзывчивых серверах. Используйте http.NewRequestWithContext для прикрепления context для отмены/таймаутов. ВСЕГДА defer resp.Body.Close() для избежания утечек подключений. Для production переиспользуйте один http.Client (он управляет pooling подключений). http.Get — ярлык, но без таймаута и кастомизации.

go
package main

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "net/http"
    "time"
)

func main() {
    // Simple GET
    resp, err := http.Get("https://api.example.com/users")
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close() // ALWAYS close the body

    var users []map[string]any
    json.NewDecoder(resp.Body).Decode(&users)
    fmt.Println(users)

    // Custom client with timeout (default has NO timeout!)
    client := &http.Client{Timeout: 30 * time.Second}

    // POST with JSON body and context
    body, _ := json.Marshal(map[string]string{"name": "Bob"})
    req, _ := http.NewRequestWithContext(context.Background(),
        "POST", "https://api.example.com/users", bytes.NewReader(body))
    req.Header.Set("Content-Type", "application/json")

    resp, err = client.Do(req)
    defer resp.Body.Close()
    fmt.Println("Status:", resp.Status)
}

Паттерн Middleware

Middleware обёртывает обработчики для добавления cross-cutting concerns (логирование, auth, CORS, rate limiting) без модификации самого обработчика. Сигнатура func(http.Handler) http.Handler — стандартный тип middleware. Chain применяет их по порядку (внешний первым). Этот паттерн — основа фреймворков вроде Chi, Echo и Gin. ResponseWriter можно обернуть для захвата status codes для логирования.

go
package main

import (
    "log"
    "net/http"
    "time"
)

// Middleware wraps an http.Handler to add cross-cutting concerns
type Middleware func(http.Handler) http.Handler

func Logging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r) // call the wrapped handler
        log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
    })
}

func Auth(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.Header.Get("Authorization") == "" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        next.ServeHTTP(w, r)
    })
}

// Chain multiple middlewares
func Chain(h http.Handler, mws ...Middleware) http.Handler {
    for i := len(mws) - 1; i >= 0; i-- {
        h = mws[i](h)
    }
    return h
}

func main() {
    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello!"))
    })
    // Apply middlewares: Auth -> Logging -> handler
    http.Handle("/", Chain(handler, Logging, Auth))
    http.ListenAndServe(":8080", nil)
}

Раздача статических файлов и шаблоны

http.FileServer раздаёт статические файлы из каталога; http.StripPrefix удаляет префикс пути для корректного разрешения путей файлов. html/template рендерит HTML безопасно (авто-экранирование для предотвращения XSS). template.Must паникует при ошибках парсинга (нормально для запуска). Шаблоны используют {{.Field}} для данных и {{range}} для итерации. Для production рассмотрите встраивание файлов через go:embed вместо чтения с диска.

go
package main

import (
    "html/template"
    "net/http"
)

func main() {
    // Serve static files (CSS, JS, images)
    fs := http.FileServer(http.Dir("./static"))
    http.Handle("/static/", http.StripPrefix("/static/", fs))

    // HTML templates
    tmpl := template.Must(template.ParseFiles("templates/index.html"))

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        data := struct {
            Title string
            Items []string
        }{
            Title: "My Page",
            Items: []string{"Apple", "Banana", "Cherry"},
        }
        tmpl.Execute(w, data)
    })

    http.ListenAndServe(":8080", nil)
}

// templates/index.html:
// <h1>{{.Title}}</h1>
// <ul>{{range .Items}}<li>{{.}}</li>{{end}}</ul>

Graceful Shutdown и go:embed

go:embed встраивает файлы в скомпилированный бинарник на этапе сборки — обеспечивая истинное single-binary развёртывание без внешних файловых зависимостей. Используйте директивы //go:embed над объявлением var. embed.FS — read-only виртуальная файловая система. fs.Sub создаёт под-файловую систему (убирает префикс каталога). Работает для статических ресурсов, HTML-шаблонов, SQL-миграций, конфиг-файлов и более.

go
package main

import (
    "embed"
    "io/fs"
    "net/http"
)

//go:embed static/*
var staticFiles embed.FS

func main() {
    // Embed static files into the binary (no external files needed)
    // Access the embedded filesystem
    sub, _ := fs.Sub(staticFiles, "static")
    http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(sub))))

    // go:embed benefits:
    // - Single binary deployment (no external files)
    // - Files are compiled into the executable
    // - Works with templates, configs, migrations, etc.

    // Embed a single file
    //go:embed config.json
    // var configData []byte

    // Embed with pattern
    //go:embed templates/*.html
    // var templates embed.FS

    http.ListenAndServe(":8080", nil)
}
16

Пакет Sync

sync.Mutex и sync.RWMutex

sync.Mutex обеспечивает эксклюзивную блокировку — только одна горутина может держать её за раз. sync.RWMutex разрешает нескольких конкурентных читателей ИЛИ одного эксклюзивного писателя — используйте, когда чтения значительно превосходят записи. Всегда парируйте Lock с defer Unlock для предотвращения дедлоков при панике функции. Встраивайте mutex в структуру (строчная mu) для приватности. Никогда не копируйте mutex (всегда используйте указатели).

go
package main

import (
    "fmt"
    "sync"
)

type SafeCounter struct {
    mu    sync.Mutex
    count int
}

func (c *SafeCounter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock() // always unlock with defer
    c.count++
}

func (c *SafeCounter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.count
}

// RWMutex: allows multiple readers OR one writer
type SafeCache struct {
    mu   sync.RWMutex
    data map[string]string
}

func (c *SafeCache) Get(key string) (string, bool) {
    c.mu.RLock()         // read lock (multiple readers OK)
    defer c.mu.RUnlock()
    val, ok := c.data[key]
    return val, ok
}

func (c *SafeCache) Set(key, val string) {
    c.mu.Lock()          // write lock (exclusive)
    defer c.mu.Unlock()
    c.data[key] = val
}

sync.Map (конкурентная мапа)

sync.Map — конкурентно-безопасная мапа, оптимизированная для специфичных сценариев: write-once-read-many (кэши) или disjoint-key доступ между горутинами. Избегает блокировок на чтение через атомарные операции. Однако для общего назначения конкурентных мап обычная мапа, защищённая sync.RWMutex, часто быстрее и эргономичнее. API sync.Map использует any (interface{}) для ключей и значений, теряя type safety.

go
package main

import (
    "fmt"
    "sync"
)

func main() {
    var m sync.Map

    // Store and Load (thread-safe, no locks needed)
    m.Store("name", "Alice")
    m.Store("age", 30)

    val, ok := m.Load("name")
    fmt.Println(val, ok) // Alice true

    // LoadOrStore: atomic get-or-set
    actual, loaded := m.LoadOrStore("name", "Bob")
    fmt.Println(actual, loaded) // Alice true (already existed)

    // Delete
    m.Delete("age")

    // Range over all entries
    m.Range(func(key, value any) bool {
        fmt.Printf("%v = %v\n", key, value)
        return true // continue; return false to stop
    })

    // Use sync.Map when:
    // 1. Keys are written once, read many times (caches)
    // 2. Multiple goroutines read/write disjoint keys
    // For most cases, a regular map + Mutex is simpler and faster
}

sync.Cond (условные переменные)

sync.Cond реализует условные переменные — горутины ждут, пока условие станет истинным. Wait() атомарно освобождает блокировку и засыпает; Signal() будит одного ожидающего, Broadcast() всех. Всегда используйте цикл for вокруг Wait() (не if) для обработки spurious wakeups. Cond полезен для producer-consumer очередей и ожидания изменений состояния, хотя каналы часто предоставляют более простую альтернативу.

go
package main

import (
    "fmt"
    "sync"
    "time"
)

type Queue struct {
    items []int
    mu    sync.Mutex
    cond  *sync.Cond
}

func NewQueue() *Queue {
    q := &Queue{}
    q.cond = sync.NewCond(&q.mu) // cond is tied to the mutex
    return q
}

func (q *Queue) Put(item int) {
    q.mu.Lock()
    defer q.mu.Unlock()
    q.items = append(q.items, item)
    q.cond.Signal() // wake one waiting goroutine
}

func (q *Queue) Get() int {
    q.mu.Lock()
    defer q.mu.Unlock()
    for len(q.items) == 0 {
        q.cond.Wait() // atomically unlocks mu, sleeps, re-locks on wake
    }
    item := q.items[0]
    q.items = q.items[1:]
    return item
}

func main() {
    q := NewQueue()
    go func() {
        time.Sleep(1 * time.Second)
        q.Put(42)
    }()
    fmt.Println("Waiting for item...")
    fmt.Println("Got:", q.Get()) // blocks until item available
}

sync.Pool (переиспользование объектов)

sync.Pool переиспользует объекты для снижения выделений и давления GC — идеально для часто выделяемых короткоживущих объектов вроде bytes.Buffer. Get() возвращает pooled-объект (вызывает New если пусто); Put() возвращает его. Пулы очищаются во время GC, поэтому не полагайтесь на них для персистентности. Всегда сбрасывайте объекты перед переиспользованием. Стандартная библиотека использует sync.Pool широко (http, json, fmt).

go
package main

import (
    "bytes"
    "sync"
)

var bufPool = sync.Pool{
    New: func() any {
        return new(bytes.Buffer) // create when pool is empty
    },
}

func Process(data []byte) string {
    // Get a buffer from the pool (or create via New)
    buf := bufPool.Get().(*bytes.Buffer)
    defer bufPool.Put(buf) // return it when done

    buf.Reset() // clear before reuse
    buf.Write(data)
    buf.WriteString("-processed")
    return buf.String()
}

// Benefits:
// - Reduces GC pressure by reusing objects
// - Avoids allocation overhead for short-lived objects
// - Pool entries can be garbage collected between GC cycles
//
// Use for: bytes.Buffer, json encoders, temporary slices
// NOT for: long-lived objects or when size is unpredictable

errgroup (группа с распространением ошибок)

errgroup (из golang.org/x/sync) — WaitGroup, который возвращает первую ошибку и может отменить оставшиеся горутины через context. g.Go() запускает горутину; g.Wait() блокирует и возвращает первую non-nil ошибку. WithContext создаёт context, отменяемый при ошибке любой горутины — поэтому другие горутины останавливаются рано. Это идиоматичный способ запуска параллельных операций, которые должны все успех или все провалиться вместе.

go
package main

import (
    "context"
    "fmt"
    "golang.org/x/sync/errgroup"
    "net/http"
)

// errgroup is like WaitGroup but propagates the first error
// and can cancel remaining goroutines on failure
func fetchAll(urls []string) ([]*http.Response, error) {
    g, ctx := errgroup.WithContext(context.Background())
    results := make([]*http.Response, len(urls))

    for i, url := range urls {
        i, url := i, url // capture loop variables
        g.Go(func() error {
            req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
            resp, err := http.DefaultClient.Do(req)
            if err != nil {
                return err // cancels ctx, stopping other goroutines
            }
            results[i] = resp
            return nil
        })
    }

    if err := g.Wait(); err != nil {
        return nil, err // first error returned
    }
    return results, nil
}

func main() {
    urls := []string{
        "https://example.com",
        "https://golang.org",
    }
    resps, err := fetchAll(urls)
    fmt.Println(len(resps), err)
}
17

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

Generic-функции и параметры типа

Дженерики Go (1.18+) используют параметры типа в квадратных скобках: [T any] объявляет T как любой тип. Компилятор выводит типы из аргументов, поэтому редко указываете явно. Дженерики обеспечивают type-safe переиспользуемые функции вроде Map/Filter/Reduce без interface{} и type assertions. 'any' — псевдоним для interface{}, введённый с дженериками.

go
package main

import "fmt"

// Type parameters in brackets: [T any] means T can be any type
func Map[T, U any](slice []T, fn func(T) U) []U {
    result := make([]U, len(slice))
    for i, v := range slice {
        result[i] = fn(v)
    }
    return result
}

func Filter[T any](slice []T, predicate func(T) bool) []T {
    var result []T
    for _, v := range slice {
        if predicate(v) {
            result = append(result, v)
        }
    }
    return result
}

func Reduce[T, U any](slice []T, initial U, fn func(U, T) U) U {
    result := initial
    for _, v := range slice {
        result = fn(result, v)
    }
    return result
}

func main() {
    nums := []int{1, 2, 3, 4, 5}
    squared := Map(nums, func(n int) int { return n * n })
    fmt.Println(squared) // [1 4 9 16 25]

    evens := Filter(nums, func(n int) bool { return n%2 == 0 })
    fmt.Println(evens) // [2 4]

    sum := Reduce(nums, 0, func(acc, n int) int { return acc + n })
    fmt.Println(sum) // 15

    // Works with any type
    words := []string{"go", "rust", "python"}
    lengths := Map(words, func(s string) int { return len(s) })
    fmt.Println(lengths) // [2 4 6]
}

Ограничения типа и comparable

Ограничения типа ограничивают, какие типы принимает generic. 'comparable' — встроенное ограничение для типов, поддерживающих == (нужно для ключей мапы и сравнений). Кастомные ограничения используют union типов (int | float64) для разрешения конкретных типов. Пакет golang.org/x/exp/constraints предоставляет Ordered (для <, > и т.д.). Ограничения могут комбинировать наборы типов и методы.

go
package main

import "fmt"

// comparable: types that support == and != (built-in constraint)
func Contains[T comparable](slice []T, target T) bool {
    for _, v := range slice {
        if v == target {
            return true
        }
    }
    return false
}

// Custom constraint with a type set
type Number interface {
    int | int64 | float64 | float32
}

func Sum[T Number](nums []T) T {
    var total T
    for _, n := range nums {
        total += n
    }
    return total
}

// Multiple type parameters
func Pair[T, U any](first T, second U) struct{ First T; Second U } {
    return struct{ First T; Second U }{first, second}
}

func main() {
    fmt.Println(Contains([]string{"a", "b", "c"}, "b")) // true
    fmt.Println(Contains([]int{1, 2, 3}, 5))             // false

    fmt.Println(Sum([]int{1, 2, 3}))           // 6
    fmt.Println(Sum([]float64{1.5, 2.5}))      // 4

    p := Pair("Alice", 30)
    fmt.Println(p) // {Alice 30}
}

Generic структуры данных

Generic-типы (Stack[T any]) создают type-safe структуры данных, работающие с любым типом без interface{} boxing или type assertions. Параметр типа T — часть типа, поэтому Stack[int] и Stack[string] — различные, проверяемые на этапе компиляции типы. Это устраняет целый класс runtime type-ошибок. Generic-структуры, методы и интерфейсы поддерживают параметры типа.

go
package main

import "fmt"

// Generic Stack — works with any type, type-safe
type Stack[T any] struct {
    items []T
}

func (s *Stack[T]) Push(item T) {
    s.items = append(s.items, item)
}

func (s *Stack[T]) Pop() (T, bool) {
    if len(s.items) == 0 {
        var zero T
        return zero, false
    }
    index := len(s.items) - 1
    item := s.items[index]
    s.items = s.items[:index]
    return item, true
}

func (s *Stack[T]) Len() int {
    return len(s.items)
}

func main() {
    // Type-safe stack of ints
    intStack := &Stack[int]{}
    intStack.Push(1)
    intStack.Push(2)
    val, _ := intStack.Pop()
    fmt.Println(val) // 2

    // Type-safe stack of strings
    strStack := &Stack[string]{}
    strStack.Push("hello")
    s, _ := strStack.Pop()
    fmt.Println(s) // hello

    // intStack.Push("oops") // compile error! type-safe
}

Generic-ограничения с ~ (базовый тип)

Префикс ~ в ограничении совпадает с любым типом, базовый тип которого — именованный тип — поэтому ~string совпадает и со string, и с type MyString string. Без ~ ограничение совпадает только с точным именованным типом, что редко полезно для кастомных типов. Используйте ~, когда хотите, чтобы дженерики работали с type aliases и именованными типами, производными от примитивов (распространено в domain modeling).

go
package main

import "fmt"

// ~ allows any type whose UNDERLYING type matches
// (e.g., type MyString string would match ~string)
type StringLike interface {
    ~string
}

func Join[S StringLike](items []S, sep string) string {
    result := ""
    for i, item := range items {
        if i > 0 {
            result += sep
        }
        result += string(item) // convert underlying type to string
    }
    return result
}

// Numeric constraint with ~
type Integer interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64
}

func Double[T Integer](v T) T {
    return v * 2
}

// Custom type based on int
type Score int

func main() {
    type MyString string
    items := []MyString{"a", "b", "c"}
    fmt.Println(Join(items, "-")) // a-b-c

    s := Double(Score(21))
    fmt.Println(s) // 42
}

Дженерики против интерфейсов (когда использовать)

Выбирайте дженерики, когда алгоритм идентичен для всех типов (коллекции, математика, преобразования) — они обеспечивают compile-time type safety с нулевым runtime overhead. Выбирайте интерфейсы, когда разным типам нужны разные реализации (полиморфизм) или когда нужна runtime-диспетчеризация (dependency injection, mocking). Они комплементарны: дженерики могут быть ограничены интерфейсами (T Stringer) для лучшего из обоих миров.

go
package main

import "fmt"

// GENERICS: best for algorithms/data structures that work
// the same way regardless of type (collections, sorting, mapping)
func Max[T int | float64](a, b T) T {
    if a > b {
        return a
    }
    return b
}

// INTERFACES: best when different types need DIFFERENT implementations
// (polymorphism, dependency injection, mocking)
type Stringer interface {
    String() string
}

type Dog struct{ Name string }
func (d Dog) String() string { return "Dog: " + d.Name }

type Cat struct{ Name string }
func (c Cat) String() string { return "Cat: " + c.Name }

func Print[T Stringer](v T) {
    fmt.Println(v.String())
}

func main() {
    fmt.Println(Max(3, 7))       // 7
    fmt.Println(Max(3.14, 2.71)) // 3.14

    Print(Dog{"Rex"}) // Dog: Rex
    Print(Cat{"Whiskers"}) // Cat: Whiskers
}

// RULE OF THUMB:
// - Same logic, different types -> Generics
// - Different logic, shared contract -> Interfaces
// - Need runtime polymorphism -> Interfaces
// - Need compile-time type safety -> Generics
18

Рефлексия

reflect.Type и reflect.Value

reflect.TypeOf возвращает runtime-тип; reflect.ValueOf возвращает runtime-значение. Из Type можно инспектировать поля структуры, методы и теги. Из Value можно читать и (с CanSet) модифицировать поля. Рефлексия мощная, но медленная (в 10-100 раз медленнее прямого доступа) и обходит compile-time type safety — используйте скупо, в основном для сериализации, ORM и фреймворков.

go
package main

import (
    "fmt"
    "reflect"
)

type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

func main() {
    u := User{Name: "Alice", Age: 30}

    // Get type and value at runtime
    t := reflect.TypeOf(u)
    v := reflect.ValueOf(u)

    fmt.Println("Type:", t.Name())        // User
    fmt.Println("Kind:", t.Kind())         // struct
    fmt.Println("NumFields:", t.NumField())

    // Iterate struct fields
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        value := v.Field(i)
        tag := field.Tag.Get("json")
        fmt.Printf("%s (%s) = %v [json:%s]\n",
            field.Name, field.Type, value, tag)
    }
    // Name (string) = Alice [json:name]
    // Age (int) = 30 [json:age]
}

Модификация значений через рефлексию

Для модификации значения через рефлексию нужно передать указатель и вызвать .Elem() для разыменования. CanSet() сообщает, можно ли назначить поле (экспортируемые поля addressable значения). Unexported (строчные) поля можно читать, но не устанавливать. SetInt/SetString/Set проверяются на этапе выполнения и паникуют при несоответствии. Мутация через рефлексию — основа конфиг-парсеров и ORM.

go
package main

import (
    "fmt"
    "reflect"
)

func main() {
    type Config struct {
        Port int
        Host string
    }

    cfg := Config{Port: 8080, Host: "localhost"}

    // MUST pass a pointer to modify (ValueOf of a struct is a copy)
    v := reflect.ValueOf(&cfg).Elem()

    // Check if a field is settable
    portField := v.FieldByName("Port")
    fmt.Println("Settable:", portField.CanSet()) // true

    // Modify fields by name
    portField.SetInt(9090)
    v.FieldByName("Host").SetString("0.0.0.0")

    fmt.Println(cfg) // {9090 0.0.0.0}

    // Set via interface (type-checked at runtime)
    v.FieldByName("Port").Set(reflect.ValueOf(7070))
    fmt.Println(cfg) // {7070 0.0.0.0}

    // Unexported fields are NOT settable (CanSet returns false)
}

Динамический вызов методов

Рефлексия может вызывать методы динамически по имени — полезно для RPC-фреймворков, плагинных систем и роутинга. MethodByName возвращает Value; Call() вызывает его со срезом аргументов reflect.Value и возвращает срез результатов. NumMethod/Method перечисляют методы. Вызовы методов через рефлексию медленные и обходят type safety, поэтому используйте только когда имя метода неизвестно на этапе компиляции.

go
package main

import (
    "fmt"
    "reflect"
)

type Calculator struct{}

func (c Calculator) Add(a, b int) int { return a + b }
func (c Calculator) Mul(a, b int) int { return a * b }

func main() {
    calc := Calculator{}
    v := reflect.ValueOf(calc)

    // Find a method by name
    method := v.MethodByName("Add")
    fmt.Println("Found:", method.IsValid()) // true

    // Call with arguments (each arg must be a reflect.Value)
    args := []reflect.Value{
        reflect.ValueOf(3),
        reflect.ValueOf(4),
    }
    results := method.Call(args)
    fmt.Println("3 + 4 =", results[0].Int()) // 7

    // List all methods
    t := reflect.TypeOf(calc)
    for i := 0; i < t.NumMethod(); i++ {
        m := t.Method(i)
        fmt.Printf("Method: %s, Type: %v\n", m.Name, m.Type)
    }
    // Method: Add, Type: func(main.Calculator, int, int) int
    // Method: Mul, Type: func(main.Calculator, int, int) int
}

Практическое применение: структура в мапу (на основе тегов)

Этот паттерн — итерация полей структуры и чтение тегов — так работают encoding/json, YAML-парсеры, ORM и библиотеки валидации под капотом. reflect позволяет писать generic-код, обрабатывающий любую структуру на основе её тегов. Конвенция тега json:'-' (пропустить это поле) стандартна. Это одно из самых легитимных использований рефлексии в Go.

go
package main

import (
    "fmt"
    "reflect"
)

// Convert any struct to a map using struct tags as keys
func StructToMap(obj any, tag string) map[string]any {
    result := make(map[string]any)
    v := reflect.ValueOf(obj)
    t := reflect.TypeOf(obj)

    // Dereference pointers
    if v.Kind() == reflect.Ptr {
        v = v.Elem()
        t = t.Elem()
    }

    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        // Use the tag value if present, else the field name
        key := field.Tag.Get(tag)
        if key == "" {
            key = field.Name
        }
        // Skip fields tagged with "-"
        if key == "-" {
            continue
        }
        result[key] = v.Field(i).Interface()
    }
    return result
}

type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
    Pass string `json:"-"` // excluded
}

func main() {
    u := User{Name: "Alice", Age: 30, Pass: "secret"}
    m := StructToMap(u, "json")
    fmt.Println(m) // map[age:30 name:Alice]
}
19

Сборка и инструментирование

go build, go run и go install

go build компилирует в исполняемый файл; go run компилирует во временный файл и запускает (отлично для разработки); go install помещает бинарник в $GOPATH/bin для глобального доступа. Выдающаяся возможность Go — лёгкая кросс-компиляция через GOOS/GOARCH — без toolchain. -ldflags='-s -w' удаляет отладочную информацию для ~30% меньших бинарников. -X инъектирует значения (например, строки версии) на этапе сборки для CI/CD.

go
# Build an executable (outputs to current directory)
go build -o myapp              # custom output name
go build -o bin/myapp ./cmd    # build a specific package

# Build for different OS/arch (cross-compilation)
GOOS=linux GOARCH=amd64 go build -o myapp-linux
GOOS=windows GOARCH=amd64 go build -o myapp.exe
GOOS=darwin GOARCH=arm64 go build -o myapp-mac  # Apple Silicon

# Run without building an executable (compiles to temp, runs, deletes)
go run main.go                 # run single file
go run ./cmd/server            # run a package

# Install: builds and puts binary in $GOPATH/bin
go install ./cmd/myapp         # now 'myapp' is on your PATH

# Build flags
go build -v                    # verbose: print packages being compiled
go build -race                 # enable race detector (for testing)
go build -ldflags="-s -w"      # strip debug info (smaller binary)
go build -ldflags="-X main.Version=1.0.0"  # inject version at build time

go mod (управление модулями)

go mod управляет зависимостями через go.mod (список зависимостей) и go.sum (контрольные суммы для безопасности). go get добавляет/обновляет зависимости; go mod tidy синхронизирует файлы (запускайте перед коммитами). Go использует Semantic Import Versioning: v2+ требует суффикса пути /v2. Кэш модулей разделяется в $GOPATH/pkg/mod. go mod vendor создаёт каталог vendor/ для воспроизводимых, офлайн или аудированных сборок.

go
# Initialize a new module
go mod init github.com/user/myproject

# This creates go.mod:
# module github.com/user/myproject
# go 1.21

# Add a dependency (auto-added to go.mod)
go get github.com/gin-gonic/gin@latest
go get github.com/lib/[email protected]  # specific version

# Tidy: add missing deps, remove unused ones
go mod tidy

# Download dependencies to local cache
go mod download

# Verify dependencies haven't been modified
go mod verify

# Vendor: copy deps into ./vendor (for offline builds)
go mod vendor

# Upgrade dependencies
go get -u github.com/lib/pq       # upgrade to latest minor
go get -u=patch github.com/lib/pq # upgrade patches only
go get github.com/lib/[email protected]  # pin specific version

# View dependency graph
go mod graph | head

go test и бенчмаркинг

go test запускает _test.go файлы; функции TestXxx(t *testing.T) — тесты, BenchmarkXxx(b *testing.B) — бенчмарки. -race включает детектор гонок данных (необходимо для конкурентного кода). -cover показывает покрытие тестами; -coverprofile генерирует детальный отчёт. Бенчмарки запускают b.N итераций, где N автонастраивается. Table-driven tests (срезы input/expected) — идиоматичный стиль тестирования Go.

go
# Run all tests in the current package
go test ./...

# Verbose output
go test -v ./...

# Run a specific test
go test -run TestAdd -v

# Run benchmarks
go test -bench=. -benchmem

# Race detector (find data races)
go test -race ./...

# Coverage
go test -cover ./...
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out  # view in browser

# --- test file example (xxx_test.go) ---
# func TestAdd(t *testing.T) {
#     got := Add(2, 3)
#     if got != 5 {
#         t.Errorf("Add(2,3) = %d, want 5", got)
#     }
# }
#
# func BenchmarkAdd(b *testing.B) {
#     for i := 0; i < b.N; i++ {
#         Add(2, 3)
#     }
# }

go fmt, go vet и golangci-lint

go fmt/gofmt обеспечивает единый канонический формат — в Go нет споров о форматировании. go vet ловит распространённые баги (несоответствия printf, копирование lock, плохие struct tags). golangci-lint агрегирует десятки линтеров и является индустриальным стандартом для CI. Запускайте gofmt и go vet перед каждым коммитом; добавьте golangci-lint в CI для глубокого анализа. Консистентное инструментирование — главная причина, почему Go-кодбазы выглядят единообразно.

go
# go fmt: format code (the ONE true Go style)
go fmt ./...              # format all files
gofmt -d main.go          # show diff without changing
gofmt -w main.go          # write changes in place

# go vet: static analysis for common mistakes
go vet ./...              # check all packages
# Catches: printf format mismatches, unreachable code,
# struct tag errors, lock copies, shadowed variables

# golangci-lint: meta-linter (runs many linters)
# Install: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
golangci-lint run         # run all enabled linters
golangci-lint run --enable=gosec,goconst  # enable specific linters

# Common linters:
# errcheck    - check unchecked errors
# gosec       - security issues
# govet       - go vet checks
# staticcheck - advanced static analysis
# ineffassign - detect ineffectual assignments
# unused      - find unused code

# goimports: fmt + auto-manage imports
# go install golang.org/x/tools/cmd/goimports@latest
goimports -w main.go

Профилирование и pprof

pprof — встроенный профайлер Go — CPU, память, goroutine и mutex-профилирование. Для долгоживущих серверов импортируйте _ 'net/http/pprof' для доступа к /debug/pprof/ endpoint для live-профилирования без перезапуска. go tool pprof даёт интерактивную оболочку (top, list, web) или web UI. Execution tracer (go tool trace) визуализирует планирование горутин и блокировки. Профилирование необходимо для performance-critical Go-кода.

go
# CPU profiling
go test -cpuprofile=cpu.prof -bench=.
go tool pprof cpu.prof
# Interactive commands: top, list FunctionName, web (graphviz)

# Memory profiling
go test -memprofile=mem.prof -bench=.
go tool pprof mem.prof

# Built-in HTTP pprof endpoint (for production servers)
import _ "net/http/pprof"
# Then visit: http://localhost:8080/debug/pprof/
# Capture a 30-second CPU profile:
# go tool pprof http://localhost:8080/debug/pprof/profile?seconds=30

# In code:
import "runtime/pprof"
f, _ := os.Create("cpu.prof")
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
// ... code to profile ...

# Trace (execution tracer)
go test -trace=trace.out -bench=.
go tool trace trace.out  # opens in browser
20

База данных (SQL)

Основы database/sql

database/sql — стандартный интерфейс для SQL-баз данных. Драйвер импортируется для побочных эффектов (саморегистрация). sql.Open не устанавливает подключение; используйте Ping для проверки. Всегда defer db.Close().

go
import "database/sql"
import _ "github.com/lib/pq"
db, err := sql.Open("postgres", "host=localhost dbname=mydb")
if err != nil { log.Fatal(err) }
defer db.Close()
err = db.Ping()

Запрос строк

Query возвращает несколько строк. Всегда defer rows.Close() для освобождения ресурсов. Scan копирует значения столбцов в переменные. Проверяйте rows.Err() после цикла на ошибки итерации.

go
rows, err := db.Query("SELECT id, name FROM users WHERE age > $1", 18)
if err != nil { log.Fatal(err) }
defer rows.Close()
for rows.Next() {
    var id int; var name string
    if err := rows.Scan(&id, &name); err != nil { log.Fatal(err) }
    fmt.Printf("%d: %s\n", id, name)
}

Запрос одной строки

QueryRow возвращает одну строку. Scan возвращает sql.ErrNoRows, если нет совпадения. Чище, чем Query, для однострочных запросов. Всегда обрабатывайте ErrNoRows явно.

go
var name string
err := db.QueryRow("SELECT name FROM users WHERE id = $1", 1).Scan(&name)
if err != nil {
    if err == sql.ErrNoRows { fmt.Println("Not found") } else { log.Fatal(err) }
}

Подготовленные операторы

Prepare создаёт переиспользуемый оператор, улучшая производительность повторяющихся запросов. Предотвращает SQL-инъекции. Всегда defer stmt.Close(). Используйте для запросов, выполняемых多次.

go
stmt, err := db.Prepare("INSERT INTO users(name) VALUES($1)")
if err != nil { log.Fatal(err) }
defer stmt.Close()
_, err = stmt.Exec("Alice")
_, err = stmt.Exec("Bob")

Транзакции

Begin начинает транзакцию. Все операции внутри tx атомарны. defer Rollback безопасен: это no-op после Commit. Если любая операция не удалась, Rollback отменяет все изменения.

go
tx, err := db.Begin()
if err != nil { log.Fatal(err) }
defer tx.Rollback()  // Safe to call after commit
_, err = tx.Exec("UPDATE accounts SET bal = bal - 100 WHERE id = 1")
_, err = tx.Exec("UPDATE accounts SET bal = bal + 100 WHERE id = 2")
if err != nil { log.Fatal(err) }
err = tx.Commit()
21

Тестирование (углублённо)

Базовый тест

Тестовые функции начинаются с Test и принимают *testing.T. t.Errorf логирует ошибку и продолжает. t.Fatalf логирует и останавливает. Запускайте через go test. Используйте testify/assert для более чистых утверждений.

go
func TestAdd(t *testing.T) {
    got := Add(2, 3)
    want := 5
    if got != want {
        t.Errorf("Add(2,3) = %d; want %d", got, want)
    }
}

Table-driven тесты

Table-driven тесты идиоматичны в Go. Определите тестовые случаи как срез структур. Итерируйте и запускайте каждый. t.Run создаёт подтесты для индивидуальных ошибок.

go
func TestAdd(t *testing.T) {
    tests := []struct {
        a, b, want int
    }{
        {1, 2, 3}, {-1, 1, 0}, {0, 0, 0},
    }
    for _, tt := range tests {
        got := Add(tt.a, tt.b)
        if got != tt.want {
            t.Errorf("Add(%d,%d)=%d; want %d", tt.a, tt.b, got, tt.want)
        }
    }
}

Подтесты

Подтесты используют t.Run с именем и функцией. Запускайте конкретные подтесты через -run pattern. Обеспечивает лучшую организацию и вывод тестов.

go
func TestAdd(t *testing.T) {
    t.Run("positive", func(t *testing.T) {
        if Add(1, 2) != 3 { t.Error("failed") }
    })
    t.Run("negative", func(t *testing.T) {
        if Add(-1, -2) != -3 { t.Error("failed") }
    })
}
// Run specific: go test -run TestAdd/negative

Test Main

TestMain запускается один раз для пакета, заменяя стандартный test runner. Используйте для глобальной настройки/очистки. Должен вызывать m.Run() для выполнения тестов. os.Exit распространяет exit code.

go
func TestMain(m *testing.M) {
    setup()
    code := m.Run()
    teardown()
    os.Exit(code)
}
func setup() { /* initialize DB, etc. */ }
func teardown() { /* cleanup */ }

Mocking интерфейсов

Mocking в Go полагается на интерфейсы. Определите интерфейс, реализуйте mock и инъектируйте. Инструменты вроде mockery и mockgen авто-генерируют mock. Позволяет unit-тестирование без внешних зависимостей.

go
type Store interface { Get(id int) (string, error) }
type MockStore struct { data map[int]string }
func (m *MockStore) Get(id int) (string, error) {
    if v, ok := m.data[id]; ok { return v, nil }
    return "", errors.New("not found")
}
// Use in tests by injecting MockStore
22

Бенчмаркинг

Базовый бенчмарк

Бенчмарк-функции начинаются с Benchmark и принимают *testing.B. b.N настраивается фреймворком для стабильных измерений. Запускайте через go test -bench. Вывод показывает ns/op (наносекунд на операцию).

go
func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Add(2, 3)
    }
}
// Run: go test -bench=.
// Output: BenchmarkAdd-8    1000000000    0.3 ns/op

Под-бенчмарки

Под-бенчмарки используют b.Run. ResetTimer исключает время настройки. ReportAllocs показывает выделения памяти. Сравнивайте производительность для разных размеров входа.

go
func BenchmarkSort(b *testing.B) {
    sizes := []int{100, 1000, 10000}
    for _, size := range sizes {
        b.Run(fmt.Sprintf("size-%d", size), func(b *testing.B) {
            data := generateData(size)
            b.ResetTimer()
            for i := 0; i < b.N; i++ {
                Sort(data)
            }
        })
    }
}

Выделения памяти

ReportAllocs показывает выделения памяти на операцию. Снижение выделений — ключ к производительности Go. Используйте sync.Pool, предварительно выделяйте срезы и избегайте лишней конкатенации строк.

go
func BenchmarkConcat(b *testing.B) {
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
        s := "a" + "b" + "c"
        _ = s
    }
}
// Output includes allocs/op and B/op

Параллельные бенчмарки

RunParallel запускает бенчмарки конкурентно через несколько горутин. Полезно для тестирования thread-safe кода. pb.Next() итерирует распределение работы. Измеряет throughput при конкурентности.

go
func BenchmarkParallel(b *testing.B) {
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            Add(2, 3)
        }
    })
}

Сравнение бенчмарков

benchstat статистически сравнивает результаты бенчмарков. Запускайте бенчмарки многократно с -count=10 для надёжного сравнения. Помогает верифицировать улучшения производительности и обнаруживать регрессии.

go
# Run with memory stats
benchstat old.txt new.txt
# Compare two runs
# name      old time/op  new time/op  delta
# Add-8     2.3ns        1.8ns        -21.7%
23

Профилирование (pprof)

CPU-профилирование

StartCPUProfile записывает CPU-профиль в файл. StopCPUProfile flush'ит данные. Анализ через go tool pprof. Используйте команды top, list, web в pprof. Фокусируйтесь на функциях, потребляющих больше всего CPU.

go
import "runtime/pprof"
f, _ := os.Create("cpu.prof")
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
// Run your code here
// Analyze: go tool pprof cpu.prof

Профилирование памяти

WriteHeapProfile захватывает текущее состояние кучи. Вызовите runtime.GC() сначала для точных результатов. Установите MemProfileRate = 1 для профилирования каждого выделения (медленнее, но точнее).

go
// Add to code
runtime.GC()
f, _ := os.Create("mem.prof")
pprof.WriteHeapProfile(f)
f.Close()
// Or use runtime.MemProfileRate = 1 for all allocations

HTTP-профилирование

net/http/pprof регистрирует profiling endpoints на стандартном mux. Доступ к профилям через HTTP без перезапуска. Полезно для production-диагностики. Защищайте endpoint в production.

go
import _ "net/http/pprof"
go func() {
    log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// Analyze live:
// go tool pprof http://localhost:6060/debug/pprof/profile

Команды pprof

top показывает функции, потребляющие больше всего ресурсов. list показывает аннотированный исходный код. web открывает SVG call graph. tree показывает иерархию вызовов. Используйте focus для фильтрации.

go
# Start interactive pprof
go tool pprof cpu.prof
(pprof) top 10        # Top functions by CPU
(pprof) list Add      # Show source with timings
(pprof) web           # Open graph in browser
(pprof) tree          # Call tree
(pprof) png > out.png # Save graph as PNG

Trace

runtime/trace захватывает трассировку выполнения: планирование горутин, GC, блокировки syscall. go tool trace открывает web UI. Полезно для диагностики задержек и проблем конкурентности.

go
import "runtime/trace"
f, _ := os.Create("trace.out")
trace.Start(f)
defer trace.Stop()
// Run code
// View: go tool trace trace.out
24

Build Tags

Базовые build tags

Build tags условно компилируют файлы. Синтаксис //go:build (Go 1.17+) заменяет // +build. Теги могут быть OS (linux, darwin, windows), arch (amd64, arm64) или кастомные.

go
//go:build linux
// +build linux

package main
// This file only compiles on Linux

Несколько тегов

&& (запятая в старом синтаксисе) требует все теги. || (пробел в старом синтаксисе) требует любой тег. ! отрицает. Используйте для платформо-специфичных реализаций.

go
//go:build linux && amd64
// +build linux,amd64

//go:build linux || darwin
// +build linux darwin

//go:build !windows
// +build !windows

Кастомные теги

Кастомные теги включают опциональный функционал. Сборка с флагом -tags. Частое использование: debug-сборки, экспериментальные функции, enterprise vs community editions. Имена тегов строчные.

go
//go:build debug
// +build debug

package main
const DebugMode = true
// Build with: go build -tags debug

Суффиксы файлов

Суффиксы файлов предоставляют неявные build-ограничения. _linux, _darwin, _windows для OS. _amd64, _arm64 для архитектуры. Проще, чем теги, для платформо-специфичного кода.

go
// file: utils_linux.go    -> Linux only
// file: utils_darwin.go   -> macOS only
// file: utils_windows.go  -> Windows only
// file: utils_amd64.go    -> amd64 only
// file: utils_debug.go    -> requires -tags debug

Сборка с тегами

Используйте -tags для включения условной компиляции. Несколько тегов через пробел. Теги версии Go (go1.18) включают version-specific код. Проверяйте через go list -tags.

go
# Build with debug tag
go build -tags debug
# Multiple tags
go build -tags "debug verbose"
# List available tags
go list -tags ./...
# Conditional in code
//go:build go1.18  // Requires Go 1.18+
25

Основы CGO

Базовый CGO

CGO позволяет вызывать C из Go. C-код в комментариях над import "C". Импорт должен быть сразу после комментария. CGO замедляет сборки и предотвращает кросс-компиляцию.

go
/*
#include <stdio.h>
void hello() {
    printf("Hello from C!\n");
}
*/
import "C"
func main() {
    C.hello()
}

Передача строк

C.CString выделяет C-строку (нужно освободить через C.free). CString копирует данные. Используйте unsafe.Pointer для преобразования. Всегда освобождайте C-выделенную память для избежания утечек.

go
/*
#include <string.h>
int len(const char* s) { return strlen(s); }
*/
import "C"
import "unsafe"
s := "Hello"cs := C.CString(s)
defer C.free(unsafe.Pointer(cs))
length := C.len(cs)

Вызов C-библиотек

#cgo LDFLAGS линкует C-библиотеки. #cgo CFLAGS устанавливает флаги компилятора. Подключайте системные заголовки через #include. CGO связывает Go и существующие C-библиотеки вроде libm, libcrypto.

go
/*
#cgo LDFLAGS: -lm
#include <math.h>
*/
import "C"
result := float64(C.sqrt(16.0))
fmt.Println(result)  // 4

C в Go callback

Go-функции, экспортированные через //export, можно вызывать из C. Функция должна быть в package main. Позволяет C-библиотекам вызывать обратно Go. Используется в FFI-биндингах.

go
/*
extern void goCallback(int);
void callGo(int n) { goCallback(n); }
*/
import "C"
//export goCallback
func goCallback(n C.int) {
    fmt.Printf("Called with %d\n", n)
}

Заметки о производительности

CGO-вызовы имеют значительный overhead по сравнению с Go-вызовами функций. Избегайте в performance-critical коде. Группируйте операции для снижения cross-boundary вызовов. Установите CGO_ENABLED=0 для чистых Go-сборок.

go
// CGO calls have overhead (~100ns)
// Avoid in hot paths
// Batch C calls to amortize cost
// CGO disables inlining and some optimizations
// Build: CGO_ENABLED=1 go build
26

Веб-фреймворки

net/http сервер

net/http — стандартная библиотека HTTP-сервера. HandleFunc регистрирует обработчики. ListenAndServe запускает сервер. Стандартный mux подходит для простых приложений; используйте кастомный mux для production.

go
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, %s!", r.URL.Path[1:])
})
http.HandleFunc("/api", apiHandler)
log.Fatal(http.ListenAndServe(":8080", nil))

Фреймворк Gin

Gin — высокопроизводительный HTTP-фреймворк. Param извлекает URL-параметры. JSON сериализует ответы. Gin предоставляет роутинг, middleware и JSON-валидацию. Быстрее net/http благодаря httprouter.

go
import "github.com/gin-gonic/gin"
r := gin.Default()
r.GET("/users/:id", func(c *gin.Context) {
    id := c.Param("id")
    c.JSON(200, gin.H{"user": id})
})
r.Run(":8080")

Middleware

Middleware обёртывает обработчики для добавления cross-cutting concerns: логирование, auth, CORS. c.Next() вызывает следующий обработчик. gin.Recovery() предотвращает краши от паник. Порядок важен.

go
func Logger() gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        c.Next()
        fmt.Printf("%s %s %v\n", c.Request.Method, c.URL.Path, time.Since(start))
    }
}
r := gin.New()
r.Use(Logger(), gin.Recovery())

Фреймворк Echo

Echo — другой популярный фреймворк, похожий на Gin. Обработчики возвращают ошибки для централизованной обработки. Встроенное middleware для CORS, JWT, rate limiting. Чистый API-дизайн.

go
import "github.com/labstack/echo/v4"
e := echo.New()
e.GET("/users/:id", func(c echo.Context) error {
    id := c.Param("id")
    return c.JSON(200, map[string]string{"user": id})
})
e.Start(":8080")

Статические файлы

FileServer раздаёт статические файлы. StripPrefix корректирует URL-путь. Полезно для раздачи HTML, CSS, JS и изображений. Для production используйте CDN или nginx для статических ресурсов.

go
// net/http
fs := http.FileServer(http.Dir("./static"))
http.Handle("/static/", http.StripPrefix("/static/", fs))
// Gin
r.Static("/assets", "./assets")
r.StaticFile("/favicon.ico", "./favicon.ico")
27

gRPC

Определение Proto

Protocol Buffers определяют контракт сервиса. proto3 — последний синтаксис. service определяет RPC-методы. message определяет структуры данных. Генерируйте Go-код через protoc.

go
syntax = "proto3";
package greet;
service Greeter {
    rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest { string name = 1; }
message HelloReply { string message = 1; }

Генерация кода

protoc генерирует Go-код из .proto файлов. --go_out генерирует типы сообщений. --go-grpc_out генерирует заглушки сервиса. Сгенерированный код не редактируется вручную.

go
# Install protoc and plugins
protoc --go_out=. --go-grpc_out=. \
    greet.proto
# This generates greet.pb.go and greet_grpc.pb.go
# Containing message types and service interfaces

Реализация сервера

Встраивайте UnimplementedGreeterServer для прямой совместимости. Реализуйте методы сервиса. grpc.NewServer создаёт сервер. Зарегистрируйте сервис перед обслуживанием.

go
type server struct { greet.UnimplementedGreeterServer }
func (s *server) SayHello(ctx context.Context, in *greet.HelloRequest) (*greet.HelloReply, error) {
    return &greet.HelloReply{Message: "Hello " + in.Name}, nil
}
lis, _ := net.Listen("tcp", ":50051")
grpc.NewServer().Serve(lis)

Клиент

grpc.Dial устанавливает подключение. WithInsecure отключает TLS (используйте credentials.NewTLS для production). Заглушка клиента предоставляет типизированные методы. Подключения pooled и переиспользуются.

go
conn, _ := grpc.Dial("localhost:50051", grpc.WithInsecure())
client := greet.NewGreeterClient(conn)
resp, _ := client.SayHello(context.Background(), &greet.HelloRequest{Name: "Alice"})
fmt.Println(resp.Message)

Стриминг

gRPC поддерживает три паттерна стриминга. Ключевое слово stream отмечает стриминг. Server streaming: один запрос, много ответов. Bidirectional: обе стороны стримят. Полезно для данных в реальном времени.

go
// Server streaming
rpc LotsOfReplies(HelloRequest) returns (stream HelloReply);
// Client streaming
rpc LotsOfGreetings(stream HelloRequest) returns (HelloReply);
// Bidirectional
rpc BidiHello(stream HelloRequest) returns (stream HelloReply);
28

Обёртывание ошибок

Обёртывание ошибок

Используйте глагол %w для обёртывания ошибок, сохраняя оригинал. Это создаёт цепочку ошибок. Избегайте %v для обёртывания, так как теряется цепочка. Обёртывание добавляет контекст без потери корневой причины.

go
if err != nil {
    return fmt.Errorf("failed to open config: %w", err)
}

Разворачивание

errors.Is проверяет, соответствует ли любая ошибка в цепочке. errors.As извлекает конкретный тип ошибки из цепочки. Используйте Is для sentinel-значений, As для типизированных ошибок. Оба обходят цепочку обёртывания.

go
err := someOperation()
if errors.Is(err, sql.ErrNoRows) {
    // Handle not found
}
var pathErr *fs.PathError
if errors.As(err, &pathErr) {
    fmt.Println("Path:", pathErr.Path)
}

Пользовательские ошибки

Пользовательские типы ошибок реализуют интерфейс error. Они несут структурированные данные для обработки ошибок. Используйте errors.As для извлечения пользовательского типа. Предпочитайте типизированные ошибки строковым сравнениям.

go
type ValidationError struct {
    Field string
    Msg   string
}
func (e *ValidationError) Error() string {
    return fmt.Sprintf("%s: %s", e.Field, e.Msg)
}
func validate(s string) error {
    if s == "" { return &ValidationError{Field: "name", Msg: "required"} }
    return nil
}

Sentinel-ошибки

Sentinel-ошибки — пакетные error-переменные. Используйте для ожидаемых ошибок. Проверяйте через errors.Is, никогда через ==. Экспортируйте их для проверки пользователями.

go
var ErrNotFound = errors.New("not found")
func Find(id int) (*User, error) {
    if id > 100 { return nil, ErrNotFound }
    return &User{}, nil
}
// Check with errors.Is
if errors.Is(err, ErrNotFound) { /* ... */ }

Паттерны обработки ошибок

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

go
// Immediate handling
if err := doSomething(); err != nil {
    return fmt.Errorf("operation failed: %w", err)
}
// Deferred error checking (e.g., Close)
func readFile() (err error) {
    f, e := os.Open("file.txt")
    if e != nil { return e }
    defer func() {
        if cerr := f.Close(); err == nil { err = cerr }
    }()
    // ...
}
29

Распространённые подводные камни

Утечки горутин

Горутины утекают, когда блокируются навсегда. Всегда предоставляйте путь выхода: отмена context, закрытие каналов или буферизованные каналы. Используйте runtime.NumGoroutine() для обнаружения утечек.

go
// BUG: goroutine leaks if receiver stops
func send(ch chan int) {
    go func() { ch <- 1 }()  // Blocks forever
}
// FIX: use context or buffered channel
func send(ctx context.Context, ch chan int) {
    go func() {
        select {
        case ch <- 1:
        case <-ctx.Done():
        }
    }()
}

Закрытие канала

Только отправитель должен закрывать канал, никогда получатель. Закрытие сигнализирует об отсутствии значений. Получение из закрытого канала возвращает нулевое значение. Отправка в закрытый канал паникует.

go
// Only the sender should close a channel
// Closing from receiver causes panic
ch := make(chan int)
go func() {
    defer close(ch)  // Sender closes
    for i := 0; i < 5; i++ { ch <- i }
}()
for v := range ch { fmt.Println(v) }

Захват переменной цикла

До Go 1.22 переменные цикла разделялись между итерациями. Горутины, захватывающие их, видят финальное значение. Go 1.22+ исправляет это, создавая новую переменную на итерацию. Передавайте как параметр для старых версий.

go
// BUG (Go < 1.22): all goroutines see last value
for i := 0; i < 3; i++ {
    go func() { fmt.Println(i) }()  // Prints 3,3,3
}
// FIX: pass as parameter
for i := 0; i < 3; i++ {
    go func(i int) { fmt.Println(i) }(i)
}

Конкурентность мап

Мапы не безопасны для конкурентного использования. Конкурентные чтения и записи вызывают runtime panic. Используйте sync.Mutex для явной блокировки или sync.Map для read-heavy конкурентного доступа.

go
// BUG: concurrent map writes panic
m := map[int]int{}
go func() { m[1] = 1 }()
go func() { m[2] = 2 }()
// FIX: use sync.Map or mutex
var mu sync.Mutex
mu.Lock(); m[1] = 1; mu.Unlock()

Nil-интерфейс

Nil-указатель, обёрнутый в интерфейс, не nil. Интерфейс имеет тип, даже если значение nil. Всегда возвращайте nil напрямую, не nil типизированный указатель. Проверяйте через reflect или возвращайте nil явно.

go
// BUG: nil check fails
type MyError struct{}
func (e *MyError) Error() string { return "err" }
func doSomething() error {
    var err *MyError = nil
    return err  // Non-nil interface!
}
// FIX: return nil explicitly
func doSomething() error {
    return nil
}

Связанные сниппеты Go

Copy-paste ready code for common tasks.

goroutine

Реализация конкурентности с помощью горутин.

channel

Общение между горутинами с помощью каналов.

select

Мультиплексирование каналов с помощью select.

mutex Мьютекс

Защита общих данных с помощью sync.Mutex.

defer Отложенный вызов

Использование и порядок выполнения defer.

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

Паттерн обработки ошибок в Go.

interface

Определение и реализация интерфейсов.

Встраивание структур

Реализация композиции через встраивание.

Дженерики

Использование дженериков в Go 1.18+.

context

Управление таймаутом и отменой с помощью context.

Файловые операции

Операции чтения и записи файлов.

HTTP-сервер

Создание HTTP-сервера.

HTTP-клиент

Отправка HTTP-запросов.

Кодирование/декодирование JSON

Преобразование между структурами и JSON.

Обработка строк

Распространённые операции в пакете strings.

Операции со срезами

Распространённые операции со срезами.

Операции с map

CRUD-операции с map.

Обработка времени

Распространённые операции в пакете time.

Регулярные выражения

Использование пакета regexp.

Тестирование

Написание модульных тестов.

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.