День 3: Указатели, передача параметров и value vs pointer semantics
🎯 Темы дня
- Передача параметров: всегда по значению
- Value vs Pointer semantics — когда что использовать
- Method sets: receiver value vs pointer
- Практика: бенчмарки и анализ аллокаций
📖 1. Передача параметров — всегда по значению
Главное правило
В Go все параметры передаются по значению. Даже указатели копируются (копируется сам указатель, а не то, на что он указывает).
gofunc foo(x int) { x = 100 } func main() { a := 42 foo(a) fmt.Println(a) // 42 — не изменилось }
Передача указателя
gofunc foo(x *int) { *x = 100 } func main() { a := 42 foo(&a) fmt.Println(a) // 100 — изменилось }
Что произошло: Скопировался указатель (8 байт), но он указывает на ту же ячейку памяти.
Передача слайсов и map
gofunc modifySlice(s []int) { s[0] = 100 // ✅ меняет оригинал } func modifyMap(m map[string]int) { m["a"] = 100 // ✅ меняет оригинал }
Почему работает: Слайс и map — это заголовки (структуры с указателями). Копируется заголовок, но он указывает на те же данные.
Но:
gofunc appendSlice(s []int) { s = append(s, 4) // ❌ НЕ меняет оригинал (если cap не хватило) }
📖 2. Value vs Pointer semantics
Когда использовать value (значение)
Критерии:
- Маленький размер (< 64 байт, примерно)
- Не нужно менять снаружи
- Immutable семантика (value objects)
- Zero value корректен
Примеры:
gotype Point struct { X, Y float64 } type Config struct { Timeout time.Duration Retries int } func (p Point) Distance() float64 { /* ... */ }
Когда использовать pointer (указатель)
Критерии:
- Большой размер (> 64 байт)
- Нужно менять состояние
- Mutable семантика (entities)
- Zero value некорректен (требует инициализации)
- Реализует интерфейс с methods, меняющими состояние
Примеры:
gotype User struct { ID int64 Name string // ... много полей } type Database struct { conn *sql.DB // ... } func (u *User) SetName(name string) { u.Name = name } func (db *Database) Close() error { /* ... */ }
Таблица сравнения
| Критерий | Value T | Pointer *T |
|---|---|---|
| Размер | Маленький (< 64 байт) | Большой |
| Мутация | Не нужно менять | Нужно менять |
| Семантика | Immutable, value objects | Entities с состоянием |
| Zero value | Корректное значение | nil, требует инициализации |
| Копирование | Дешёвое | Дешёвое (копируется указатель) |
| GC нагрузка | Нет (если в stack) | Есть (если в heap) |
📖 3. Method sets
Правило method sets
- У типа
T— только методы с receiver(T) - У типа
*T— методы с receiver(T)И(T*)
gotype User struct{ Name string } func (u User) GetName() string { return u.Name } func (u *User) SetName(name string) { u.Name = name } var u = User{} u.GetName() // ✅ u.SetName("Alice") // ✅ (Go автоматически берет &u) var p = &User{} p.GetName() // ✅ p.SetName("Bob") // ✅
Но для интерфейсов — строже!
gotype Namer interface { GetName() string SetName(string) } var u = User{} var n Namer = u // ❌ ошибка: User не реализует SetName var n2 Namer = &u // ✅ *User реализует оба метода
Почему: Интерфейс хранит значение. Если тип User не имеет метода SetName (он есть только у *User), то User не реализует интерфейс.
Практическое правило
Если хотя бы один метод имеет pointer receiver — все методы должны иметь pointer receiver. Иначе будет путаница.
💻 Практика (1–2 часа)
Задание 1: Бенчмарк value vs pointer
Сравни производительность передачи большой структуры по значению и по указателю:
gotype BigStruct struct { Data [1024]byte } func processByValue(s BigStruct) int { return int(s.Data[0]) } func processByPointer(s *BigStruct) int { return int(s.Data[0]) } func BenchmarkByValue(b *testing.B) { s := BigStruct{} for i := 0; i < b.N; i++ { processByValue(s) } } func BenchmarkByPointer(b *testing.B) { s := &BigStruct{} for i := 0; i < b.N; i++ { processByPointer(s) } }
Ожидаемый результат: ByPointer быстрее, потому что копируется только 8 байт (указатель), а не 1024 байта.
Задание 2: Анализ аллокаций
Запусти go build -gcflags="-m" и определи, что уходит в heap:
gofunc foo() { u1 := User{Name: "Alice"} // value u2 := &User{Name: "Bob"} // pointer processByValue(u1) processByPointer(u2) var i interface{} = u1 // ⚠️ }
Ожидаемый вывод:
u1→ heap (передаётся вinterface{})u2→ heap (указатель)
Задание 3: Method sets
Определи, какие типы реализуют интерфейс:
gotype Reader interface { Read() string } type MyReader struct{} func (r MyReader) Read() string { return "value" } type MyPtrReader struct{} func (r *MyPtrReader) Read() string { return "pointer" } func main() { var r1 Reader = MyReader{} // ✅ var r2 Reader = &MyReader{} // ✅ var r3 Reader = MyPtrReader{} // ❌ var r4 Reader = &MyPtrReader{} // ✅ }
Объясни, почему r3 не компилируется.
🗣️ Каверзные вопросы
-
Почему в Go нет передачи по ссылке? Ответ: В Go есть только передача по значению. "Ссылки" (указатели) — это тоже значения, которые копируются.
-
Что быстрее: передать большую структуру по значению или по указателю? Ответ: По указателю быстрее, потому что копируется только 8 байт, а не вся структура.
-
Почему
for _, v := rangeна больших структурах — это медленно? Ответ: Потому чтоv— копия структуры. Используйfor i := rangeиitems[i]. -
Можно ли вызвать метод с pointer receiver на value? Ответ: Да, Go автоматически берёт адрес. Но для интерфейсов — только если тип указателя.
-
Что выведет?
gotype User struct{ Name string } func (u User) SetName(name string) { u.Name = name } u := User{Name: "Alice"} u.SetName("Bob") fmt.Println(u.Name)Ответ: "Alice" — receiver по значению, копия не меняет оригинал.
-
Когда использовать
*Tв return? Ответ: Когда структура большая, или когда нужно вернутьnil(например, для интерфейсов).
🧩 Проверь себя
✅ Чек-лист дня
- Понимаю, что все параметры передаются по значению
- Знаю, когда использовать value, а когда pointer
- Понимаю rule of thumb: < 64 байт → value, > 64 байт → pointer
- Могу объяснить method sets для value vs pointer receivers
- Запустил бенчмарк value vs pointer и сравнил результаты
- Понимаю, почему
for _, v := rangeна больших структурах — это медленно