Go: функции, указатели и семантика значений
В Go аргументы передаются по значению всегда: функция получает копию каждого аргумента. Для указателя копируется адрес, поэтому через него можно изменить объект вызывающего кода. Срез, map, channel, function и interface также передаются как значения-дескрипторы, но часто содержат ссылку на общее состояние. Различие «по значению или по ссылке» недостаточно: нужно назвать, что именно копируется и что остаётся разделённым.
Зачем это на интервью
Эта модель нужна, чтобы объяснить, почему swap(a, b) не меняет переменные, почему изменение элемента среза видно снаружи, почему append иногда не виден вызывающему коду и когда pointer receiver меняет method set. Это один из главных индикаторов идиоматичного Go.
Минимум для E4
- Утверждать: Go передаёт аргументы по значению; указатель — тоже значение.
- Различать
T,*T,&x,*p,new(T)и zero value указателяnil. - Возвращать изменённый срез после
appendлибо принимать*[]T, когда это действительно оправдано. - Применять multiple return и проверять
errorсразу после вызова.
package main
import "fmt"
type Counter struct{ N int }
func bumpValue(c Counter) { c.N++ }
func bumpPointer(c *Counter) { c.N++ }
func add(xs []int, x int) []int { return append(xs, x) }
func main() {
c := Counter{}
bumpValue(c)
bumpPointer(&c)
xs := []int{1}
xs = add(xs, 2)
fmt.Println(c.N, xs)
}Функции — значения первого класса: их можно присваивать переменным, передавать и возвращать. Замыкание захватывает переменные, а не снимок их значений; при concurrent использовании захваченное изменяемое состояние требует синхронизации.
Функции могут быть variadic: параметр xs ...T внутри имеет тип []T, а готовый slice передают как f(xs...). Это удобно для адаптеров и логирования, но API не должен неявно менять переданный slice. Named results — обычные локальные переменные, полезные при коротком defer, изменяющем возвращаемую ошибку. Избегайте return без аргументов в длинной функции и не затеняйте named err через :=: читатель легко потеряет фактически возвращаемое значение.
func join(sep string, parts ...string) string { return strings.Join(parts, sep) }
func closeWithError(c io.Closer) (err error) {
defer func() { if closeErr := c.Close(); err == nil { err = closeErr } }()
return doWork(c)
}Углубление для E5/Senior
Выбор T против *T — контракт, а не микрооптимизация. Значение подходит для маленького неизменяемого объекта, естественного zero value и независимой копии. Указатель выражает общую идентичность, возможность мутации, отсутствие (nil) или нежелательность копирования. Публичный API должен быть последовательным: смешанные receivers без причины затрудняют использование через интерфейсы и порождают неочевидные копии.
Методы с receiver T входят в method set и T, и *T; методы с receiver *T — только в method set *T. Компилятор может неявно взять адрес адресуемой переменной для вызова v.PointerMethod(), но это не означает, что T удовлетворяет интерфейсу, требующему такой метод. На границе интерфейса проверяется method set, а не удобство конкретного вызова.
Указатель сам по себе не гарантирует производительность: он может привести к escape на heap, ухудшить locality и добавить nil-пути. Не копируйте объекты с lock-полями; для остальных сначала определите ownership и измерьте go test -bench с -benchmem, профилем и escape analysis (go build -gcflags=-m).
Ключевые понятия
- Pass by value — копируется значение аргумента при вызове функции.
- Pointer — значение-адрес объекта типа
T;*Tможет бытьnil. - Dereference —
*pобращается к значению по указателю; разыменование nil вызывает panic. - Reference-like descriptor — срез/map/channel/function могут разделять внутреннее состояние при копировании дескриптора.
- Receiver — первый неявный параметр метода, значимый для method set и мутации.
- Closure — функция вместе с захваченными переменными окружения.
- Variadic parameter — последний параметр
...T, принимающий ноль или больше аргументов. - Named result — именованная возвращаемая переменная; её значение возвращает bare
return. - Escape — решение компилятора разместить значение не на стеке, если его время жизни этого требует.
Типовые вопросы
- Есть ли в Go передача по ссылке?
- Нет в смысле механизма вызова: всё передаётся по значению. Но копия указателя или дескриптора может указывать на общее изменяемое состояние.
- Почему
append(s, x)часто надо вернуть?appendможет создать новый backing array и всегда возвращает заголовок с новой длиной; вызывающий должен получить этот заголовок.
- Когда выбрать pointer receiver?
- Когда метод меняет receiver, объект нельзя копировать либо нужна общая идентичность. Не выбирайте его автоматически для каждой структуры.
- Почему
Tне реализует интерфейс с методомfunc (*T) M()?- Method set значения
Tне содержит методы с pointer receiver, хотя адресуемую переменнуюTможно удобно вызвать напрямую.
- Method set значения
- Что захватывает замыкание?
- Переменные окружения. Последующее изменение переменной видно замыканию; при нескольких goroutine это может быть race.
- Заменяет ли указатель возврат ошибки?
- Нет.
nilможет означать отсутствие результата, но причина ошибки должна быть явной черезerror, когда она важна вызывающему коду.
- Нет.
Практика
- Реализуйте
AppendUnique(xs []string, s string) []string. Готово: входной срез не меняется непреднамеренно, caller присваивает результат, тесты покрывают spare capacity и отсутствие capacity. - Напишите тип счётчика с value и pointer receiver. Готово: тесты демонстрируют копирование, изменение через указатель и проверку реализации интерфейса на compile time.
- Сделайте фабрику замыкания
newAdder. Готово: тесты объясняют захват состояния, а concurrent-вариант защищён mutex либо не разделяет состояние и проходит-race.
Частые ошибки и ловушки
- Говорить «срез передаётся по ссылке» и не учитывать копию заголовка и realloc при
append. - Игнорировать возвращаемый
errorили откладывать его проверку до использования результата. - Разыменовывать nil pointer вместо явной валидации входа.
- Использовать pointer receiver только ради «избежать копии», не учитывая API и escape.
- Захватывать переменную цикла или общее изменяемое значение в goroutine без понимания времени жизни и синхронизации.
Связанные темы
Go · Основы языка Go · Срезы, map и строки · Структуры, embedding и теги · Методы и интерфейсы