Публичный API Go-пакета

Зачем это на интервью

Публичный пакет — это долгосрочный контракт для кода, который вы не контролируете. На интервью важно показать, что вы ограничиваете поверхность API, делаете нормальный путь простым и заранее думаете о nil, zero value, ошибках и миграции.

Минимум для E4

Экспортируйте только необходимое; имена должны объяснять назначение без чтения реализации. Экспортируемый идентификатор требует doc comment, начинающийся с его имени. Предпочитайте конкретные типы и функции-конструкторы, скрывайте поля, если инвариант нельзя сохранить при произвольной записи. Определите, что значит zero value и допустим ли nil.

package retry
 
import (
	"context"
	"errors"
	"fmt"
)
 
// Do calls fn at most Attempts times until it succeeds or ctx is canceled.
type Doer struct {
	Attempts int
}
 
// Do runs fn and returns its last error. A non-positive Attempts means one call.
func (d Doer) Do(ctx context.Context, fn func(context.Context) error) error {
	if fn == nil {
		return errors.New("retry: nil function")
	}
	attempts := d.Attempts
	if attempts <= 0 {
		attempts = 1
	}
	var err error
	for i := 0; i < attempts; i++ {
		if err = ctx.Err(); err != nil {
			return err
		}
		if err = fn(ctx); err == nil {
			return nil
		}
	}
	return fmt.Errorf("retry after %d attempts: %w", attempts, err)
}

Здесь zero value Doer{} осмыслен и безопасен. Do не принимает nil callback молча, сохраняет отмену context и документирует число попыток. Решение о retry по классу ошибки — отдельный контракт, который нужно добавить явно, а не угадывать.

Углубление для E5/Senior

Проектируйте API для эволюции: добавление поля в публичную struct может быть совместимо для именованных literals, но ломает неименованные; добавление метода к экспортируемому интерфейсу ломает реализации. Не возвращайте конкретные типы зависимости, если это навязывает клиентов. Для options используйте functional options лишь когда параметров действительно много и у них есть безопасные defaults; не заменяйте ими ясный конструктор из двух аргументов.

Применяйте semantic import versioning при breaking changes. До major upgrade рассмотрите новый метод/тип, deprecated-обёртку и период миграции. Документируйте concurrency safety, ownership io.Reader/io.Closer, допустимость повторного вызова и поведение при partial failure.

Ключевые понятия

  • Surface area — все экспортируемые имена; чем она меньше, тем проще поддерживать контракт.
  • Zero-value usability — нулевое значение готово к полезной, безопасной работе или явно документировано как непригодное.
  • Constructor invariant — свойство, которое можно обеспечить только при создании через конструктор.
  • Functional option — функция настройки; удобна для редких опций с defaults, но усложняет discoverability и валидацию.
  • Semantic import versioning — несовместимая major-версия использует путь модуля /vN для N >= 2.

Типовые вопросы

  1. Нужно ли всегда делать конструктор?
    • Нет: если zero value полезен и нет инвариантов, достаточно типа; конструктор нужен для обязательных зависимостей/валидации.
  2. Почему не стоит экспортировать поля?
    • Клиенты получают возможность обходить инварианты, а изменение представления становится breaking change.
  3. Как документировать nil?
    • Явно: допускается ли nil, что он означает и какая ошибка/паника будет при недопустимости.
  4. Почему нельзя добавить метод к публичному интерфейсу?
    • Все внешние реализации перестанут компилироваться; лучше вводить новый меньший interface или адаптер.
  5. Когда использовать functional options?
    • Для нескольких независимых необязательных параметров с разумными defaults; обязательные зависимости лучше оставить явными аргументами.

Практика

  • Спроектируйте клиент с обязательным baseURL, timeout по умолчанию и документированным nil-контрактом.
  • Добавьте tests для zero value, nil callback, отменённого context и последней ошибки.
  • Найдите экспортируемое поле, замените его методом/конструктором и опишите миграцию клиента.

Критерии готовности: все export comments проходят golint-эквивалент в проекте, zero value или обязательность конструктора документированы, новые ошибки классифицируемы, а изменение не ломает API без версии или адаптера.

Частые ошибки и ловушки

  • Экспортировать типы и поля «на всякий случай».
  • Делать nil неявным отключателем без документации.
  • Добавлять option после начала работы объекта, хотя она должна быть immutable.
  • Использовать panic для ошибки входных данных библиотеки вместо возврата error.

Связанные темы

Go-код на интервью · Контракты и mockability · Совместимость API

Источники