В Go нет классов и нет наследования. Методы привязываются к любому именованному типу, инкапсуляция работает на уровне пакета через заглавные буквы в именах, повторное использование даёт композиция и встраивание, а полиморфизм обеспечивают интерфейсы.
Инкапсуляция: идентификатор со строчной буквы виден только внутри своего пакета: границей инкапсуляции служит пакет, а не тип. private и protected нет; два типа из одного пакета видят неэкспортируемые поля друг друга.
Методы: объявляются вне типа, с получателем (func (a *Account) Deposit(n int)), и на любом именованном типе того же пакета, а не только на структурах.
Нет наследования: встраивание продвигает поля и методы другого типа, но внешний тип не становится подтипом: Outer нельзя передать туда, где ждут Base, и ничего не переопределяется.
Полиморфизм: через интерфейсы, которым тип соответствует неявно: функция, принимающая io.Writer, работает с файлом, буфером и сетевым соединением.
GoJunior
Структуры, методы и интерфейсы
Структуры, встраивание, наборы методов, неявные интерфейсы, type switch
Закрыто вопросов
0 / 24
- 0Повторить
- 6Часто спрашивают
Вопросы
Литерал с именами называет поля, которые задаёт, а остальные оставляет с нулевыми значениями; позиционный обязан перечислить все поля в порядке объявления. Пишите литералы с именами: поле, добавленное или переставленное позже, ломает или молча меняет смысл каждого позиционного.
Поле, добавленное в чужом пакете, превращает каждый позиционный литерал в too few values in struct literal. Два поля одного типа, переставленные местами, по-прежнему компилируются, но значения перепутаны.
Проверка composites в go vet ругается на позиционные литералы типов из других пакетов. Для маленького локального типа Point{1, 2} остаётся идиоматичным.
Внутри литералов слайса и map тип элемента можно опустить: []Point{{1, 2}, {3, 4}}.
&T{...} берёт адрес литерала напрямую; это обычный способ получить *T.
Метод можно объявить только на именованном типе, определённом в том же пакете, поэтому time.Time и int для вас закрыты. Определите свой тип (type Celsius float64 или структуру со встроенным time.Time) и объявите метод на нём.
Правило фиксирует наборы методов: импорт пакета никогда не меняет поведение типа из другого пакета, и два пакета не могут навесить на один тип конфликтующие String().
Определяемый тип вроде type Deadline time.Time сохраняет поля, но не получает ни одного исходного метода; обратно переводят конверсией time.Time(d).
Встраивание, struct{ time.Time }, сохраняет все исходные методы через продвижение и позволяет добавить новые.
Часто честным ответом будет обычная функция: func IsWeekend(t time.Time) bool.
Структура сравнима, только если сравнимо каждое её поле, поэтому одно поле-slice, map или func отбирает у всей структуры и ==, и роль ключа map, и компилятор об этом скажет.
Сравнимы: числа, строки, bool, указатели, каналы, интерфейсы (== паникует в рантайме, если динамические значения несравнимы), массивы сравнимых элементов и структуры из всего этого. == сравнивает поле за полем.
Не сравнимы: slice, map и func; их можно сравнить только с nil. Поэтому [4]string в ключе map работает, а []string нет.
Ошибки выглядят так: invalid map key type Loose и struct containing []string cannot be compared.
Указатели сравниваются по адресу: два *Point, указывающих на равные значения, не равны по ==.
Если равенство всё же нужно: slices.Equal для поля, reflect.DeepEqual в тестах или отдельная структура-ключ только из сравнимых полей.
У struct{} нет полей и она занимает ноль байт, поэтому это тип для значений, которые не несут данных: элементы множества в map[K]struct{}, сигналы в chan struct{} и типы, которые существуют ради методов.
Множества: seen[k] = struct{}{} хранит только ключ, _, ok := seen[k] проверяет наличие. В отличие от map[K]bool, нет третьего состояния, которое можно неверно прочитать.
Сигналы: done := make(chan struct{}) и close(done) будят всех получателей; тип говорит, что полезной нагрузки нет, как у ctx.Done().
Носители методов: type noopLogger struct{} реализует интерфейс без состояния, и создание его значения ничего не стоит.
Разные значения нулевого размера могут делить один адрес, поэтому их указатели никогда не сравнивайте.
Интерфейс задаёт набор сигнатур методов, и любой тип, у которого эти методы есть, соответствует ему автоматически: объявления implements нет. Тип и интерфейс могут не знать друг о друге, поэтому интерфейс можно написать даже для чужого типа.
Соответствие проверяет компилятор там, где значение присваивается интерфейсному типу, так что типизация остаётся статической; это называют структурной типизацией или статической утиной типизацией.
В Java класс обязан перечислить свои интерфейсы, и чужой класс не подойдёт к вашему интерфейсу без адаптера. В Go *os.File, *bytes.Buffer и net.Conn подходят под io.Reader, ничего не зная о вашем коде.
Благодаря этому интерфейс объявляет потребитель (то, что ему нужно), а не производитель (то, что он предлагает).
Вызов через интерфейсную переменную в рантайме уходит в метод того значения, которое в ней лежит.
Имена и сигнатуры методов должны совпадать точно. Метод с указательным получателем принадлежит только *T, поэтому обычное значение T такому интерфейсу не соответствует.
any является псевдонимом interface{}, интерфейса без методов, поэтому ему соответствует любой тип. Используйте его, когда через код проходят значения действительно неизвестного типа (fmt, JSON, значения контекста), а дженерики, когда тип различается между вызывающими, но фиксирован в каждом вызове.
Чтобы воспользоваться значением из any, нужно утверждение типа или type switch; компилятор ничего не проверяет, и неверная догадка превращается в панику в рантайме.
[]any не то же самое, что []int: для конверсии нужен цикл, и каждый элемент упаковывается в интерфейс, часто с аллокацией.
Параметр типа сохраняет тип: func Max[T cmp.Ordered](xs []T) T возвращает T и не пропустит слайс со смешанными типами ещё на этапе компиляции.
Законное место для any: fmt.Println(a ...any), разбор JSON неизвестной формы в map[string]any, разнородные контейнеры.
Практическое правило: если каждый вызывающий сразу после вызова пишет утверждение типа, вам был нужен параметр типа.
Утверждение типа проверяет один тип: s := v.(string) паникует, если внутри не string, а s, ok := v.(string) возвращает нулевое значение и false. Type switch проверяет несколько типов в одном операторе и в каждой ветке связывает значение с подходящим типом.
В панике названы оба типа: interface conversion: interface {} is int, not string.
С конкретным T выражение v.(T) требует ровно этот тип: int внутри никогда не пройдёт .(int64). С интерфейсным типом оно спрашивает, реализует ли значение этот интерфейс; так проверяют необязательное поведение, например w.(http.Flusher).
В switch x := v.(type) ветка с одним типом даёт x этот тип; ветка с несколькими (case int, int64) оставляет x интерфейсного типа.
case nil срабатывает только для интерфейса, в котором вообще нет значения.
Ветки проверяются сверху вниз, поэтому при пересекающихся интерфейсных ветках побеждает первая подошедшая.
fmt.Stringer объявлен как интерфейс из одного метода String() string, и fmt вызывает его для %v, %s и Println. Поэтому форматирование получателя через %v внутри String снова вызывает String, и так до переполнения стека.
Разорвать круг помогает определяемый тип без методов: type plainUser User, затем форматировать plainUser(u). Новый определяемый тип методы не наследует.
Или форматируйте только нужные поля, и заодно секреты вроде токенов не попадут в логи.
go vet сообщает об этом как causes recursive (user.User).String method call, и go test эту проверку запускает.
С указательным получателем String есть только у *T: печать значения T покажет сырые поля, печать &t использует String.
Если тип реализует ещё и error, fmt вызовет Error(), а не String().
Указательный получатель нужен, когда метод меняет получателя, когда в структуре есть то, что нельзя копировать, например sync.Mutex, или когда копирование заметно дорого. В остальных случаях подойдёт получатель-значение: он всегда работает с копией.
func (c Counter) Inc() { c.n++ } компилируется и ничего видимого не делает: увеличивает копию, которая исчезает на return.
Вызовы прощают многое: на переменной c.Inc() означает (&c).Inc(), а через указатель можно вызывать методы-значения. Интерфейсы не прощают, и так работает правило наборов методов.
Будьте последовательны: если одному методу нужен указательный получатель, дайте его всем, чтобы тип везде передавался как *T.
Получатель-значение подходит маленьким неизменяемым значениям вроде time.Time или Point: копия из нескольких слов дешёвая, и копии безопасно раздавать горутинам.
«Большая» структура определяется профилем, а не числом байт: указатель может заставить значение убежать в кучу.
В набор методов T входят только методы с получателем-значением; в набор методов *T входят оба вида. Соответствие интерфейсу смотрит в набор методов, поэтому у значения Worker нет Run, и интерфейс реализует только *Worker.
Прямой вызов w.Run() работает, потому что переменная w адресуемая и Go переписывает вызов в (&w).Run(). Значение внутри интерфейса хранится как скрытая копия, и такую замену Go сделать не может.
Если бы мог, Run менял бы скрытую копию, и вы никогда не прочитали бы изменение обратно: молчаливый no-op, что хуже ошибки компиляции.
Ошибка звучит так: Worker does not implement Handler (method Run has pointer receiver). Передавайте &Worker{}, а конструктор пусть возвращает *Worker.
Элементы map и результаты функций тоже не адресуемы: m["a"].Run() падает с cannot call pointer method Run on Worker.
Методы с получателем-значением есть в обоих наборах, поэтому тип только с такими методами подходит и как T, и как *T.
Конструктор представляет собой обычную функцию, по соглашению NewT (или New, если в пакете один главный тип), которая проверяет входные данные, заполняет значения по умолчанию и возвращает значение, обычно вместе с error. Функциональные опции передают необязательные настройки вариативным списком func(*T).
Сначала попробуйте сделать полезным нулевое значение, как у bytes.Buffer и sync.Mutex; тогда конструктор не нужен вовсе.
Возвращайте *T, когда у типа есть методы с указательным получателем, мьютекс или нужна идентичность; T подходит для маленьких значений вроде time.Time.
Возвращайте (*T, error), когда создание может не удаться. Вариант MustT, который паникует, оставьте для переменных уровня пакета и тестов.
Опции читаются как именованные аргументы (NewServer(addr, WithTimeout(5*time.Second))), значения по умолчанию живут в одном месте, а новая опция не ломает ни одного вызывающего.
Более простая альтернатива: структура конфигурации, NewServer(Config{...}). Опции окупаются в публичных API с множеством редко задаваемых настроек.
Она печатает outer, I am base, base. Встраивание даёт композицию, а не наследование: o.Describe() просто означает o.Base.Describe(), его получатель имеет тип Base, и внутри b.Name() вызывает Base.Name; до Outer вызов не доходит.
Продвижение: полями и методами встроенного типа можно пользоваться так, будто они объявлены на внешнем; компилятор переписывает o.Describe() в o.Base.Describe().
Затенение, а не переопределение: Outer.Name лежит на глубине 0 и скрывает продвинутый Base.Name для селекторов на Outer, но методы Base по-прежнему вызывают свои.
Конфликт имён: если у двух встроенных типов на одной глубине есть Name, не продвигается ни один. Структура компилируется; o.Name падает с ambiguous selector o.Name только там, где написан, а структура перестаёт соответствовать интерфейсу, которому нужен Name.
Диспетчеризация в стиле Go: принимайте интерфейс: func Describe(n interface{ Name() string }) вызовет тот Name, который есть у значения.
Встраивание ещё и расширяет API: встройте sync.Mutex, и любой вызывающий сможет сделать Lock на вашем типе. Если нужно переиспользование без публикации, берите именованное поле.
Структура получает все методы интерфейса, переадресованные значению, которое лежит в поле, поэтому она реализует интерфейс, а вы переопределяете только те методы, которые хотите изменить. Именно так устроен sort.Reverse: он встраивает sort.Interface и переопределяет Less.
Декораторы: чтобы запомнить статус-код ответа, встройте http.ResponseWriter и переопределите WriteHeader; всё остальное переадресуется.
Частичные фейки в тестах: встройте большой интерфейс в фейк и реализуйте только два метода, которые трогает тест.
Подвох: непереопределённый метод, вызванный при nil во встроенном поле, паникует с разыменованием nil-указателя в рантайме. В фейке это полезный громкий отказ; в рабочем коде всегда заполняйте это поле.
Скрытые необязательные интерфейсы: у обёртки есть только методы встроенного интерфейса, поэтому w.(http.Flusher) на ней не срабатывает. Добавьте метод Unwrap() http.ResponseWriter, и http.ResponseController доберётся до Flush под ней.
Встраивание time.Time продвигает его MarshalJSON в Event, поэтому сам Event реализует json.Marshaler, и json.Marshal пишет только время: "2026-10-15T09:00:00Z". name и user_id молча теряются.
Почему: продвинутые методы входят в набор методов внешнего типа, поэтому Event удовлетворяет всем интерфейсам, которым удовлетворяет time.Time: json.Marshaler, json.Unmarshaler, fmt.Stringer. encoding/json проверяет Marshaler раньше, чем смотрит на поля.
Вторая половина: UnmarshalJSON тоже продвигается: декодирование {"name":"login"} в Event падает с Time.UnmarshalJSON: input is not a JSON string, а fmt.Println(e) печатает только время.
Исправление: именованное поле At time.Time. Методы именованного поля не продвигаются, поэтому у Event нет MarshalJSON, и он кодируется поле за полем.
Как поймать: round-trip тест (Marshal, Unmarshal, сравнение) для каждого типа, который уходит по сети. На ревью встраивание в DTO типа с MarshalJSON или String служит тревожным сигналом.
Struct tag представляет собой строку, привязанную к полю, которая хранится в метаданных типа и читается в рантайме через reflect.StructField.Tag. Компилятор её не проверяет, поэтому некорректный тег молча игнорируется, и пакет откатывается к поведению по умолчанию.
Формат строгий: пары key:"value" через пробел в обратных кавычках, без пробела после двоеточия, например json:"id" db:"user_id" . Ключи вроде json и db относятся к соглашениям конкретных пакетов, а не к возможностям языка.
Пакет читает свой ключ через field.Tag.Get("json"); Lookup дополнительно сообщает, есть ли ключ вообще.
В json: "nick" (пробел после двоеточия) тег не разбирается, Get возвращает "", и encoding/json берёт ключом имя поля Nick.
Проверка structtag в go vet сообщает bad syntax for struct tag value и дубли JSON-ключей. go test эту проверку не запускает, поэтому в CI нужен go vet ./....
Опечатка в самом ключе, например jsno:"id", остаётся корректным синтаксисом тега, поэтому даже go vet молчит; поймает её только тест закодированного результата.
Добавьте рядом с типом var _ Store = (*PostgresStore)(nil). Строка присваивает интерфейсу типизированный nil-указатель, поэтому сборка падает в пакете самого типа, как только метод переименовали или его сигнатура разъехалась.
Без неё проверка происходит только там, где значение используют как Store, часто в другом пакете, который автор правки не собирает, и ошибка всплывает далеко от причины.
Ни один метод не вызывается и ничего не аллоцируется; пустой идентификатор _ держит строку вне API пакета.
Берите форму с указателем, если хоть у одного метода указательный получатель: var _ Store = PostgresStore{} упадёт, даже когда всё в порядке.
Кладите её в обычный файл, не в _test.go, чтобы расхождение ловил go build.
Читайте ошибку дальше первой строки: (missing method Put) или (wrong type for method Get) называет ровно то, что изменилось.
Объявляйте интерфейс в пакете, который его использует, и только с теми методами, которые этот пакет вызывает, а конструкторы пусть возвращают конкретные типы. Тогда потребитель зависит от маленького контракта, а производитель может добавлять методы, никого не ломая.
На стороне потребителя: пакет отчётов, который вызывает только Get, объявляет type userGetter interface{ Get(ctx context.Context, id int64) (User, error) }. Фейк для его теста занимает пять строк, а новый метод хранилища не ломает никого.
Возвращайте структуры: NewPostgresStore возвращает *PostgresStore. Вызывающие видят все методы и их документацию, могут пользоваться типом напрямую и не вынуждены идти через абстракцию, о которой не просили.
Маленькие интерфейсы композируются: у io.Reader и io.Writer по одному методу, а io.ReadWriteCloser встраивает их. Чем больше интерфейс, тем слабее абстракция.
Запах: экспортируемый интерфейс на 14 методов рядом с единственной реализацией и мок, сгенерированный по нему. Это заголовочный файл, а не абстракция.
Исключения: возвращайте интерфейс, когда конкретный тип нужно скрыть или он зависит от входных данных: error, hash.Hash из sha256.New(), net.Conn из net.Dial.
Он печатает 0, а затем 3. Вызов метода на указателе сводится к обычному вызову функции с указателем в качестве первого аргумента, поэтому nil-получатель законен; паника возникает только при разыменовании, а Sum сначала проверяет nil.
t.Sum() при var t *Tree означает (*Tree).Sum(t): в месте вызова ничего не разыменовывается.
Пригодный к работе nil-получатель используют как осознанный приём: пустое дерево или список без особых случаев, геттеры из protobuf-кода вроде GetName(), которые на nil-сообщении возвращают "".
С получателем-значением всё иначе: вызов через nil *T сначала копирует *t, и эта копия паникует с invalid memory address or nil pointer dereference.
Вызов метода на nil-интерфейсе (без типа внутри) паникует всегда: диспетчеризовать не к чему.
Значение интерфейса хранит две вещи (динамический тип и указатель на значение) и равно nil, только когда пусты обе. Возврат nil-указателя *bytes.Buffer как io.Writer заполняет тип, поэтому w == nil даёт false, а Write затем выполняется на nil-получателе.
w != nil говорит правду: интерфейс несёт «*bytes.Buffer, равный nil». Вызов спокойно уходит в (*bytes.Buffer).Write, тот разыменовывает получателя и паникует.
Форма всегда одна: переменная конкретного указательного типа, присвоенная только на части веток и возвращённая через интерфейсный результат. С error это классический err != nil для nil *MyError.
Исправляйте у источника: if !enabled { return nil }; нетипизированный nil оставляет обе части пустыми. Ещё лучше вернуть io.Discard, и вызывающему проверка вообще не нужна.
Type switch подчиняется тому же правилу: nil *bytes.Buffer пропускает case nil и попадает в case *bytes.Buffer.
Проверить значение внутри можно только рефлексией, reflect.ValueOf(w).IsNil(), и это признак неудачного API, а не исправление.
Два значения интерфейса равны, когда оба nil или когда в них лежат один и тот же динамический тип и равные значения. Если типы совпадают, но несравнимы, как слайсы или map, == компилируется, а затем паникует с comparing uncomparable type []int.
Разные динамические типы просто не равны, без всяких конверсий: any(1) == any(int64(1)) даёт false.
Указатели внутри сравниваются по адресу: два errors.New("x") различны, поэтому sentinel-ошибки и объявляют переменными уровня пакета.
Компилятор не видит динамические типы, и проверка уезжает в рантайм. Ключи map интерфейсного типа попадают в ту же ловушку: m[any([]int{1})] = 1 паникует с hash of unhashable type []int.
Сравнение интерфейса с конкретным значением, как в err == ErrNotFound, сначала приводит конкретную сторону к интерфейсному типу.
Структура внутри сравнивается поле за полем, поэтому структура с полем-слайсом паникует так же, как голый слайс.
У Get получатель по значению, поэтому каждый вызов копирует весь Cache вместе с мьютексом и блокирует копию. Get и Set никогда не исключают друг друга на общей map, и программа падает с concurrent map read and map write; копия, снятая, пока Set держит блокировку, зависает навсегда.
Map общая, блокировка своя: значение map хранит указатель на хеш-таблицу рантайма, поэтому копия видит те же данные. sync.Mutex устроен как обычная структура из двух целых чисел, и его копия становится совсем другой, никак не связанной с оригиналом блокировкой.
Почему зависает: копирование мьютекса, пока его держит Set, копирует и заблокированное состояние, и Get ждёт блокировку, которую никто никогда не отпустит. В маленькой программе это заканчивается all goroutines are asleep - deadlock!, в сервере зависают запросы и утекают горутины.
Исправление: получатель-указатель и у Get, чтобы он блокировал мьютекс вызывающего. Тип с мьютексом получает получатели-указатели во всех методах и передаётся как *Cache.
Как поймать: go vet сообщает Get passes lock by value: <pkg>.Cache contains sync.Mutex (проверка copylocks; go test её не запускает, поэтому go vet нужен в CI). go test -race показывает гонку данных даже в тех прогонах, где падения нет.
Значение интерфейса занимает два машинных слова: слово типа и указатель на данные. Для any (eface) первое слово хранит динамический тип; для интерфейса с методами (iface) оно указывает на itab с типом и таблицей методов. Значение, которое не является указателем, приходится скопировать, чтобы получить адрес.
Раскладка
itab связывает один интерфейсный тип с одним конкретным и хранит указатели на функции методов интерфейса. Те itab, что компилятор видит статически, он генерирует сам; остальные, нужные утверждениям и конверсиям из интерфейса в интерфейс, строятся один раз и кешируются в глобальной таблице.
Слово данных всегда содержит указатель. Указатели, map, каналы и функции кладутся туда напрямую, поэтому io.Writer(&buf) ничего не стоит.
Упаковка
Всё остальное копируется в память, на которую указывает интерфейс, и в профиле это видно как runtime.convT64, convTstring, convTslice или convT вместе с mallocgc.
Рантайм не аллоцирует для нулевых значений и целых чисел меньше 256, а escape-анализ может оставить копию на стеке, если интерфейс не убегает. -gcflags=-m покажет, какой у вас случай.
Диспетчеризация
Вызов загружает itab.fun[i] и прыгает по нему: несколько наносекунд, но это блокирует инлайнинг, а аргументы часто убегают в кучу, потому что компилятор не видит вызываемую функцию.
Утверждение к конкретному типу сравнивает одно слово; утверждение к интерфейсному типу требует поиска itab.
PGO (Go 1.21+) девиртуализирует горячие вызовы, у которых в профиле доминирует один конкретный тип.
Держите конкретные типы на горячем пути, а интерфейсы на границах, и подтверждайте это через go test -bench . -benchmem.
Поля кладутся в порядке объявления, каждое по смещению, кратному его собственному выравниванию. Компилятор их никогда не переставляет, поэтому выравнивающие байты убираете вы сами, объявляя широкие поля первыми.
На 64-битных платформах int64 или указатель должны начинаться на кратном 8, int32 на кратном 4, а bool где угодно. struct{ A bool; B int64; C bool; D int32 } теряет семь байт после A и три после C: всего 24 байта.
Те же поля, отсортированные от широких к узким, занимают 16. Анализатор fieldalignment из golang.org/x/tools такое находит, но go vet его не запускает.
Поле нулевого размера в конце стоит целую единицу выравнивания: иначе указатель на него смотрел бы за конец объекта, в следующую аллокацию.
Заниматься этим стоит только для типов, которые аллоцируются массово: на десяти миллионах строк 8 байт на каждой дают 80 МБ кучи. Аллокатор округляет до классов размеров, так что сжатие с 24 до 17 байт не вернёт ничего.
Порядок служит ещё и читаемости и блокировкам: держите мьютекс рядом с полями, которые он защищает.