День 3: Указатели, передача параметров и value vs pointer semantics

🎯 Темы дня

  1. Передача параметров: всегда по значению
  2. Value vs Pointer semantics — когда что использовать
  3. Method sets: receiver value vs pointer
  4. Практика: бенчмарки и анализ аллокаций

📖 1. Передача параметров — всегда по значению

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

В Go все параметры передаются по значению. Даже указатели копируются (копируется сам указатель, а не то, на что он указывает).

go
func foo(x int) {
    x = 100
}

func main() {
    a := 42
    foo(a)
    fmt.Println(a) // 42 — не изменилось
}

Передача указателя

go
func foo(x *int) {
    *x = 100
}

func main() {
    a := 42
    foo(&a)
    fmt.Println(a) // 100 — изменилось
}

Что произошло: Скопировался указатель (8 байт), но он указывает на ту же ячейку памяти.

Передача слайсов и map

go
func modifySlice(s []int) {
    s[0] = 100  // ✅ меняет оригинал
}

func modifyMap(m map[string]int) {
    m["a"] = 100  // ✅ меняет оригинал
}

Почему работает: Слайс и map — это заголовки (структуры с указателями). Копируется заголовок, но он указывает на те же данные.

Но:

go
func appendSlice(s []int) {
    s = append(s, 4)  // ❌ НЕ меняет оригинал (если cap не хватило)
}

📖 2. Value vs Pointer semantics

Когда использовать value (значение)

Критерии:

  1. Маленький размер (< 64 байт, примерно)
  2. Не нужно менять снаружи
  3. Immutable семантика (value objects)
  4. Zero value корректен

Примеры:

go
type Point struct {
    X, Y float64
}

type Config struct {
    Timeout time.Duration
    Retries int
}

func (p Point) Distance() float64 { /* ... */ }

Когда использовать pointer (указатель)

Критерии:

  1. Большой размер (> 64 байт)
  2. Нужно менять состояние
  3. Mutable семантика (entities)
  4. Zero value некорректен (требует инициализации)
  5. Реализует интерфейс с methods, меняющими состояние

Примеры:

go
type 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 TPointer *T
РазмерМаленький (< 64 байт)Большой
МутацияНе нужно менятьНужно менять
СемантикаImmutable, value objectsEntities с состоянием
Zero valueКорректное значениеnil, требует инициализации
КопированиеДешёвоеДешёвое (копируется указатель)
GC нагрузкаНет (если в stack)Есть (если в heap)

📖 3. Method sets

Правило method sets

  • У типа T — только методы с receiver (T)
  • У типа *T — методы с receiver (T) И (T*)
go
type 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") // ✅

Но для интерфейсов — строже!

go
type 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

Сравни производительность передачи большой структуры по значению и по указателю:

go
type 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:

go
func 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

Определи, какие типы реализуют интерфейс:

go
type 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 не компилируется.


🗣️ Каверзные вопросы

  1. Почему в Go нет передачи по ссылке? Ответ: В Go есть только передача по значению. "Ссылки" (указатели) — это тоже значения, которые копируются.

  2. Что быстрее: передать большую структуру по значению или по указателю? Ответ: По указателю быстрее, потому что копируется только 8 байт, а не вся структура.

  3. Почему for _, v := range на больших структурах — это медленно? Ответ: Потому что v — копия структуры. Используй for i := range и items[i].

  4. Можно ли вызвать метод с pointer receiver на value? Ответ: Да, Go автоматически берёт адрес. Но для интерфейсов — только если тип указателя.

  5. Что выведет?

    go
    type 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 по значению, копия не меняет оригинал.

  6. Когда использовать *T в return? Ответ: Когда структура большая, или когда нужно вернуть nil (например, для интерфейсов).


🧩 Проверь себя

1. Почему s[0] = 100 внутри функции меняет оригинальный слайс, а append — не всегда?
2. Что выведет код, где SetName объявлен с value receiver: u := User{Name: "Alice"}; u.SetName("Bob"); fmt.Println(u.Name)?
3. Когда стоит использовать pointer receiver? — выберите все подходящие
4. Почему for _, v := range items медленно, если items — слайс больших структур?
5. Какие методы входят в method set типа T (не указателя)?
6. Почему var n Namer = User{} не компилируется, если SetName объявлен с pointer receiver?
7. Как в Go передаются аргументы в функции?

✅ Чек-лист дня

  • Понимаю, что все параметры передаются по значению
  • Знаю, когда использовать value, а когда pointer
  • Понимаю rule of thumb: < 64 байт → value, > 64 байт → pointer
  • Могу объяснить method sets для value vs pointer receivers
  • Запустил бенчмарк value vs pointer и сравнил результаты
  • Понимаю, почему for _, v := range на больших структурах — это медленно