Начало работы
Hello World
Каждая программа Go начинается в функции main() пакета main. Используйте 'go run' для выполнения, 'go build' для компиляции. gofmt автоматически форматирует код (табы, интервалы). Оператор import подключает пакеты — fmt обрабатывает форматированный I/O.
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.goGo Modules (go.mod)
Go modules (go.mod) управляют зависимостями с Go 1.11. 'go mod init' создаёт файл модуля. 'go get' добавляет зависимости. 'go mod tidy' удаляет неиспользуемые импорты и добавляет отсутствующие. Путь модуля — путь импорта для вашего пакета.
// 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' ограничивает импорты родительским модулем. Имя пакета должно совпадать с именем каталога.
// 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 и не могут использовать :=.
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 в символ.
// 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Строки и форматирование
Основы строк
Строки Go — неизменяемые последовательности байт. len() возвращает количество байт, а не символов — используйте utf8.RuneCountInString() для Unicode. Строки сравниваются лексикографически через ==, <, >. Range по строке итерирует по rune (Unicode code point), корректно обрабатывая многобайтовые символы.
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 удаляет ведущие/завершающие пробелы. Все функции возвращают новые строки (строки неизменяемы).
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 возвращает строку вместо печати.
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²) выделения).
// 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, измените и преобразуйте обратно. Это необходимо для интернационализированной обработки текста.
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"Управление потоком
If / Else
if в Go не требует скобок вокруг условий, но фигурные скобки обязательны (даже для однострочных тел). Init-оператор (if x := f(); x > 0) ограничен блоком if/else — распространён для проверки ошибок. Этот паттерн держит область видимости переменной узкой.
// 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 работает со срезами, мапами, строками и каналами. Используйте _ для пропуска индекса или значения. Порядок итерации мапы случаен по дизайну.
// 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.
// 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 выполняются даже при панике функции.
// 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 = 2Goto, Break, Continue
break выходит из внутреннего цикла; continue переходит к следующей итерации. Метки (break outer) выходят из вложенных циклов. goto существует, но редко используется — предпочитайте структурированный поток управления. Метки также можно использовать с continue для перехода к следующей итерации внешнего цикла.
// 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.
// 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
}
}Функции
Определение и множественные возвраты
Функции Go могут возвращать несколько значений — канонический паттерн (result, error). Именованные возвраты действуют как объявленные переменные, инициализированные нулевыми значениями; 'голый' return использует их. Именованные возвраты улучшают читаемость для сложных функций, но могут запутать при чрезмерном использовании. Всегда проверяйте ошибки немедленно.
// 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'ов, итераторов и функций с состоянием.
// 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.
// 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)) // 7init() и анонимные функции
Функции init() запускаются автоматически до main(), в порядке объявления. Используйте для настройки (загрузка конфига, валидация, регистрация). Несколько init() на файл разрешены. Анонимные функции можно немедленно вызывать (IIFE) или назначать перемен ным. Также используются для горутин.
// 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 переиспользуемые структуры данных и алгоритмы без дублирования кода.
// 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-получатель, все должны. Методы можно определять на любом типе в том же пакете.
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!Структуры данных
Массивы и срезы
Массивы имеют фиксированную длину; срезы динамические (на базе массивов). append() добавляет элементы, увеличивая capacity по необходимости. make([]T, len, cap) предварительно выделяет для эффективности. Срез создаёт представление (разделяет базовый массив) — используйте copy() для независимых данных. Всегда проверяйте len/cap при оптимизации.
// 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()).
// 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 наследованию.
// 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().
// 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-срезов выделяйте каждую строку отдельно.
// 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 — сделайте копию сначала, если нужен исходный порядок.
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Методы и интерфейсы
Определение интерфейсов
Интерфейсы определяют сигнатуры методов. Go использует структурную типизацию — тип реализует интерфейс автоматически, если имеет все требуемые методы (без явного объявления 'implements'). Это обеспечивает развязанный дизайн: определяйте интерфейсы там, где используете, а не там, где реализуете.
// 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 — предпочитайте дженерики.
// 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 метода) идиоматичны.
// 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.
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.
// 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 использует композицию вместо наследования. Встраивание структуры продвигает её поля и методы во внешнюю структуру. Можно переопределить продвигаемые методы, определив метод с тем же именем на внешнем типе. Встраивание интерфейса позволяет декорирование/делегирование — внешний тип удовлетворяет интерфейсу и может перенаправлять вызовы.
// 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Обработка ошибок
Основы ошибок
Go обрабатывает ошибки как значения, а не исключения. Функции возвращают (result, error) — всегда проверяйте err != nil немедленно. errors.New() создаёт простые ошибки; fmt.Errorf() добавляет форматирование. Sentinel-ошибки (var ErrX = errors.New()) разрешают сравнение через ==. Никогда не игнорируйте ошибки (используйте _ только намеренно).
// 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, детали валидации, логика повторов.
// 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) для сохранения цепочки.
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 для: ошибок программирования (индекс вне диапазона), сбоев инициализации пакета и защиты горутин от краша программы. Предпочитайте возврат ошибок для ожидаемых сбоев.
// 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 для проверки конкретных ошибок в цепочке. Избегайте многократного обёртывания одной ошибки одним контекстом.
// 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 tracesDefer, Panic, Recover вместе
Комбинируйте defer (очистка), panic (фатальные ошибки) и recover (ловля паник) для надёжного управления ресурсами. Defers выполняются в порядке LIFO, даже при панике. Именованное возвращаемое значение (err error) можно установить внутри отложенного recover. Этот паттерн гарантирует закрытие файлов/подключений и откат транзакций даже при панике кода.
// 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)
}Конкурентность
Горутины
Горутины — лёгкие потоки Go — запускаются через 'go'. Они дешёвые (~2KB стека) и управляются планировщиком Go (M:N scheduling). Функция main не ждёт горутины — используйте sync.WaitGroup или каналы для синхронизации. Никогда не используйте time.Sleep для синхронизации в production (используйте WaitGroup).
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 по каналу до его закрытия. Каналы включают 'разделять память через общение'.
// 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, таймауты. Всегда включайте таймауты для избежания дедлоков.
// 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 конкурентности.
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'ы для защиты общего состояния. 'Разделяйте память через общение; не общайтесь через разделение памяти.'
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.
// 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
}Файловый I/O и OS
Чтение файлов
os.ReadFile() читает весь файл в память (просто, но не для больших файлов). Для больших файлов откройте через os.Open(), читайте чанками и всегда defer Close(). bufio.Scanner читает построчно — идеально для обработки текста. Всегда проверяйте ошибки, особенно io.EOF для определения завершения чтения.
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: владелец может читать/писать, остальные читать.
// 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 удаляет каталоги рекурсивно. Всегда проверяйте ошибки.
// 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 баз данных).
// 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 указывает ненулевые коды выхода. Будьте осторожны с пользовательским вводом для предотвращения инъекции команд.
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 5sJSON и кодирование
JSON Marshal и Unmarshal
encoding/json преобразует между структурами Go и JSON. Marshal (структура → JSON), Unmarshal (JSON → структура). Теги структуры (json:"name") контролируют именование полей и видимость. omitempty пропускает нулевые/пустые значения. json:"-" полностью исключает поле. Это стандартный способ обработки API-запросов/ответов в 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 emptyJSON с мапами и срезами
Для динамического JSON (неизвестная структура) unmarshal в map[string]any (или interface{}). JSON-числа становятся float64 — type-assert для доступа. json.Decoder/Encoder работают с потоками (файлы, HTTP-тела) эффективно. Используйте struct unmarshaling, когда знаете схему; используйте мапы для гибких/динамических данных.
// 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 строки.
// 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 defaultHTTP-сервер
Пакет net/http строит HTTP-серверы. http.HandleFunc регистрирует обработчики. http.ResponseWriter пишет ответ; *http.Request читает запрос. Для JSON API установите Content-Type и используйте json.NewEncoder(w).Encode(data). Стандартная библиотека готова для production — фреймворк не нужен для простых API. Используйте mux-роутеры (gorilla/mux, chi) для сложной маршрутизации.
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 для отмены.
// 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")Тестирование и бенчмаркинг
Unit-тесты
Тестовые файлы заканчиваются на _test.go, тестовые функции начинаются с Test. Table-driven tests — идиоматичный паттерн: определите тестовые случаи в срезе, цикл с t.Run для подтестов (именованные, индивидуально запускаемые). Используйте t.Errorf для ошибок (продолжает), t.Fatalf для фатальных (останавливает). Запускайте через 'go test -v' для подробного вывода.
// 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' для верификации значимых улучшений.
// 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 из интерфейсов.
// 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-утилитами.
// 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)Горутины (углублённо)
Запуск горутин и WaitGroup
Горутины — лёгкие потоки, управляемые Go runtime (не OS-потоки) — можно породить сотни тысяч. sync.WaitGroup координирует завершение горутин: Add(1) перед запуском, Done() по завершении (используйте defer), Wait() для блокировки до нуля. Всегда передавайте указатель на WaitGroup, чтобы все горутины разделяли один счётчик.
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 редко нужно настраивать.
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.
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 controlsync.Once (одноразовая инициализация)
sync.Once гарантирует, что функция выполняется ровно один раз во всех горутинах — стандартный способ реализации thread-safe singleton и ленивой инициализации. Эффективнее, чем проверка флага через mutex. Метод Do блокирует конкурентных вызывающих до завершения первого вызова. sync.Once также используется внутри многих паттернов стандартной библиотеки вроде sync.OnceValue (Go 1.21+).
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-паттерн конкурентности для ограниченного параллелизма.
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)
}
}Каналы и Select
Основы каналов (небуферизованные и буферизованные)
Небуферизованные каналы (make(chan T)) синхронизируют отправителя и получателя — отправка блокирует до готовности получателя. Буферизованные каналы (make(chan T, n)) разрешают n отправок без получателя, разъединяя тайминги отправителя/получателя. Только отправитель должен закрывать канал (сигнал 'больше нет значений'). Получение из закрытого канала возвращает нулевое значение с ok=false. Range-циклы выходят при закрытии канала.
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 паттерны строятся на нём.
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.
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 только отправка) обеспечивают использование канала на этапе компиляции. Двунаправленный канал неявно преобразуется в направленный при передаче в функцию. Это документирует намерение и предотвращает баги — функция-продюсер буквально не может получать из собственного выходного канала. Используйте направленные типы в сигнатурах функций для ясности контрактов.
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 позволяет перенести таймер.