Чистый код в Go -- это не просто форматирование. Это набор практик, которые делают код читаемым, поддерживаемым и предсказуемым. Go-сообщество уделяет этому особое внимание, потому что "gofmt's style is no one's favorite, yet gofmt is everyone's favorite".
Именование
Правило длины имён
В Go длина имени пропорциональна размеру области видимости:
// Short scope → short name
for i, v := range items { ... }
if err != nil { ... }
func (s *Server) handle(w http.ResponseWriter, r *http.Request) { ... }
// Medium scope → medium name
func processOrders(ctx context.Context, orders []*Order) error { ... }
// Long scope (package-level, exported) → descriptive name
type ConnectionPool struct { ... }
func NewHTTPClientWithRetries(cfg Config) *Client { ... }
Аббревиатуры
Общепринятые аббревиатуры пишутся одним регистром:
// Good
type HTTPClient struct{} // not HttpClient
type URLParser struct{} // not UrlParser
type XMLDecoder struct{} // not XmlDecoder
func ServeHTTP() {} // not ServeHttp
var userID string // not userId (ID is abbreviation)
// Also good for unexported
var httpClient *http.Client
var xmlData []byte
Избегайте stuttering (заикания)
// Bad: package name repeats in type name
package user
type UserService struct{} // user.UserService -- stutters!
type UserRepository struct{}
// Good: package name is part of the call
package user
type Service struct{} // user.Service -- clean!
type Repository struct{} // user.Repository
// Bad
package http
func HTTPServe() {} // http.HTTPServe
// Good
package http
func Serve() {} // http.Serve
Булевы переменные и функции
// Good: reads as English sentence
if user.IsActive() { ... }
if order.HasItems() { ... }
if cache.Contains(key) { ... }
var connected bool
var verbose bool
// Bad: confusing double negation or unclear meaning
if !user.IsNotActive() { ... } // double negative
var isFlag bool // redundant "is" for variable
Длина функций
Go-сообщество рекомендует стремиться к функциям до 30 строк. Длинные функции -- сигнал о нарушении SRP.
// Bad: 80+ lines, does too many things
func ProcessOrder(ctx context.Context, order *Order) error {
// validate order (20 lines)
// calculate totals (15 lines)
// apply discounts (20 lines)
// save to database (10 lines)
// send notification (15 lines)
// update metrics (5 lines)
return nil
}
// Good: decomposed into focused functions
func ProcessOrder(ctx context.Context, order *Order) error {
if err := order.Validate(); err != nil {
return fmt.Errorf("validating order: %w", err)
}
total := calculateTotal(order.Items)
total = applyDiscounts(total, order.Coupons)
order.Total = total
if err := s.repo.Save(ctx, order); err != nil {
return fmt.Errorf("saving order: %w", err)
}
s.notifyOrderPlaced(ctx, order) // fire and forget
return nil
}
func calculateTotal(items []Item) decimal.Decimal { ... }
func applyDiscounts(total decimal.Decimal, coupons []Coupon) decimal.Decimal { ... }
Комментарии
Godoc-конвенции
// Package user provides user management functionality including
// registration, authentication, and profile operations.
package user
// User represents a registered user in the system.
// Zero value is not valid; use New to create instances.
type User struct {
id string
name string
email string
}
// New creates a User with the given name and email.
// It generates a new UUID v7 as the user ID.
// Returns error if name is empty or email is invalid.
func New(name, email string) (*User, error) {
// ...
}
Правила:
- Комментарий начинается с имени элемента:
// User represents... - Полные предложения с точкой в конце
- Первое предложение -- краткое описание (используется в
go doc) - Объясните зачем и контракт, а не как
Когда комментировать
// Good: explains WHY, not WHAT
// retryDelay uses exponential backoff to avoid thundering herd
// when multiple clients reconnect after an outage.
func retryDelay(attempt int) time.Duration {
return time.Duration(1<<attempt) * time.Second
}
// Good: documents non-obvious behavior
// Close blocks until all pending writes are flushed.
// It is safe to call Close multiple times.
func (w *Writer) Close() error { ... }
// Bad: comment restates the code
// increment counter by 1
counter++
// Bad: obvious comment
// Check if error is nil
if err != nil { ... }
TODO и FIXME
// TODO(username): add rate limiting before launch
// FIXME(username): race condition when concurrent writes
// HACK(username): temporary workaround for upstream bug #1234
Всегда указывайте имя автора и, по возможности, ссылку на issue.
Форматирование ошибок
Go имеет чёткие конвенции для сообщений об ошибках:
// 1. Lowercase first letter
fmt.Errorf("connecting to database: %w", err) // Good
fmt.Errorf("Connecting to database: %w", err) // Bad
// 2. No trailing punctuation
fmt.Errorf("invalid email format") // Good
fmt.Errorf("Invalid email format.") // Bad
// 3. Action context, not "failed to"
fmt.Errorf("reading config file: %w", err) // Good
fmt.Errorf("failed to read config file: %w", err) // Bad (redundant)
// 4. Include relevant data
fmt.Errorf("parsing user %q: %w", username, err) // Good
// 5. Use %w for wrapping (supports errors.Is / errors.As)
fmt.Errorf("querying user %s: %w", id, err)
Пример цепочки ошибок:
// Result: "starting server: binding address :8080: listen tcp :8080: address already in use"
func startServer(addr string) error {
if err := bindAddress(addr); err != nil {
return fmt.Errorf("starting server: %w", err)
}
return nil
}
func bindAddress(addr string) error {
if err := net.Listen("tcp", addr); err != nil {
return fmt.Errorf("binding address %s: %w", addr, err)
}
return nil
}
Организация пакетов
Domain-driven (рекомендуется)
myapp/
├── user/
│ ├── user.go # User type, business logic
│ ├── store.go # Store interface
│ └── user_test.go
├── order/
│ ├── order.go
│ ├── store.go
│ └── order_test.go
├── postgres/
│ ├── user_store.go # PostgreSQL implementation of user.Store
│ └── order_store.go
├── api/
│ ├── handler.go # HTTP handlers
│ └── middleware.go
└── cmd/
└── server/
└── main.go
Layer-based (для простых проектов)
myapp/
├── handler/
│ ├── user.go
│ └── order.go
├── service/
│ ├── user.go
│ └── order.go
├── repository/
│ ├── user.go
│ └── order.go
├── model/
│ ├── user.go
│ └── order.go
└── cmd/
└── server/
└── main.go
Стандартный layout проекта
project/
├── cmd/ # Entry points (main packages)
│ ├── api-server/
│ │ └── main.go
│ └── worker/
│ └── main.go
├── internal/ # Private packages (can't be imported by other modules)
│ ├── config/
│ ├── server/
│ ├── user/
│ └── database/
├── pkg/ # Public packages (importable by other modules)
│ ├── httpclient/
│ └── validator/
├── api/ # OpenAPI specs, protobuf definitions
│ └── openapi.yaml
├── migrations/ # Database migrations
│ ├── 001_create_users.up.sql
│ └── 001_create_users.down.sql
├── configs/ # Configuration files
│ ├── config.yaml
│ └── config.example.yaml
├── go.mod
├── go.sum
├── Makefile
├── Dockerfile
└── README.md
Важно:
internal/-- пакеты, которые не могут быть импортированы извне модуля (компилятор запрещает)cmd/-- каждая поддиректория -- отдельныйmainпакетpkg/-- необязательно, используйте если есть код для повторного использования другими модулями
Guard clauses (ранний возврат)
// Bad: arrow code (deep nesting)
func authorize(user *User, resource string) error {
if user != nil {
if user.IsActive {
if user.HasPermission(resource) {
if !user.IsBlocked {
return nil // success buried deep
} else {
return ErrBlocked
}
} else {
return ErrForbidden
}
} else {
return ErrInactive
}
} else {
return ErrNoUser
}
}
// Good: guard clauses, flat code
func authorize(user *User, resource string) error {
if user == nil {
return ErrNoUser
}
if !user.IsActive {
return ErrInactive
}
if user.IsBlocked {
return ErrBlocked
}
if !user.HasPermission(resource) {
return ErrForbidden
}
return nil
}
Преимущества:
- Основной путь (happy path) всегда на минимальном уровне отступа
- Ошибки обрабатываются сразу и не накапливаются
- Легко добавить новое условие без реструктуризации
Named returns: когда использовать
// Good: named return for defer + error pattern
func readFile(path string) (content []byte, err error) {
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("opening %s: %w", path, err)
}
defer func() {
if cerr := f.Close(); cerr != nil && err == nil {
err = fmt.Errorf("closing %s: %w", path, cerr)
}
}()
return io.ReadAll(f)
}
// Bad: named returns just for "documentation"
func divide(a, b float64) (result float64, err error) { // unnecessary
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// Good: unnamed returns for simple functions
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
Именованные возвращаемые значения оправданы только когда:
- Нужно модифицировать
errвdefer(как в примере выше) - Функция возвращает несколько значений одного типа, и имена различают их
- Функция длинная и имена помогают отслеживать контракт
Избегайте глобального состояния
// Bad: package-level mutable state
var (
defaultTimeout = 30 * time.Second
globalDB *sql.DB
logger *slog.Logger
)
func init() {
var err error
globalDB, err = sql.Open("pgx", os.Getenv("DB"))
if err != nil {
log.Fatal(err)
}
}
// Bad: function depends on hidden global
func GetUser(id string) (*User, error) {
return globalDB.QueryRow("SELECT ...", id)
}
Проблемы:
- Нельзя тестировать с другой БД
- Порядок
init()непредсказуем - Скрытые зависимости
// Good: explicit dependencies
type UserService struct {
db *sql.DB
timeout time.Duration
logger *slog.Logger
}
func NewUserService(db *sql.DB, timeout time.Duration, logger *slog.Logger) *UserService {
return &UserService{db: db, timeout: timeout, logger: logger}
}
gofmt -- не обсуждается
В Go форматирование кода не является предметом дискуссий:
# Format all files in project
gofmt -w .
# Or use go fmt (wraps gofmt)
go fmt ./...
# goimports: gofmt + automatic import management
goimports -w .
Всё Go-сообщество использует один стиль. Это устраняет споры о форматировании и делает любой Go-код мгновенно узнаваемым.