Skip to content

Go – Naming Conventions

Go naming conventions are deliberately minimal and opinionated. They differ significantly from Java, Python, and C# — and those differences are intentional design decisions, not gaps to be filled with conventions from other languages. Understanding why Go names things the way it does is as important as knowing the rules.

The Golden Rule

In Go, capitalisation controls visibility. An identifier starting with an uppercase letter is exported (public). One starting with a lowercase letter is unexported (package-private). This is enforced by the compiler — there are no public or private keywords.

Quick Reference

Identifier Convention Example
Package lowercase, single word httputil, orderservice
File snake_case.go order_service.go, http_client.go
Exported type MixedCaps OrderService, PaymentProcessor
Unexported type mixedCaps orderCache, paymentState
Exported function MixedCaps NewOrderService(), ParseURL()
Unexported function mixedCaps parseResponse(), buildQuery()
Exported constant MixedCaps MaxRetryCount, DefaultTimeout
Unexported constant mixedCaps defaultPageSize, maxBufferSize
Interface (single method) Method name + er Reader, Writer, Stringer
Interface (multi method) MixedCaps noun OrderRepository, PaymentGateway
Method receiver Short abbreviation o for Order, s for Service
Local variable Short, mixedCaps n, buf, userID, orderTotal
Error variable err err, or ErrNotFound for sentinel
Test function Test<Function>_<Scenario> TestCreateOrder_WhenCartEmpty
Benchmark function Benchmark<Name> BenchmarkCalculateTotal
Example function Example<Name> ExampleOrderService_CreateOrder

The MixedCaps Rule

Go uses MixedCaps (exported) or mixedCaps (unexported) for all multi-word identifiers without exception. There are no underscores in Go identifier names — not in variables, not in functions, not in types, not in constants.

// ✅ Correct — MixedCaps throughout
type OrderService struct{}
func NewOrderService() *OrderService {}
const MaxRetryCount = 3
var defaultTimeout = 30 * time.Second

// ❌ Incorrect — underscores, snake_case, UPPER_SNAKE
type Order_Service struct{}    // underscores not used
const MAX_RETRY_COUNT = 3      // UPPER_SNAKE is not Go convention
var default_timeout = 30       // snake_case is not used in Go

Packages

Package names are the single most important naming decision in Go — they become the prefix every caller uses: http.Get, json.Marshal, fmt.Println.

  • All lowercase, no underscores, no mixedCaps
  • Single word where possible; concatenate if necessary
  • Short, concise, evocative — callers type this constantly
  • Do not repeat the package name in exported identifiers (avoid stutter)
// ✅ Correct package names
package httputil
package orderservice
package payment
package config

// ❌ Incorrect
package httpUtil        // mixedCaps not used
package order_service   // underscore not used
package util            // too generic
package common          // too generic
package interfaces      // never name a package "interfaces"

Avoid Stutter

Because callers always qualify names with the package, do not repeat the package name in the identifier:

// Package: order

// ❌ Stutter — callers write order.OrderService, order.NewOrderService
type OrderService struct{}
func NewOrderService() *OrderService {}

// ✅ No stutter — callers write order.Service, order.New
type Service struct{}
func New(repo Repository, logger *slog.Logger) *Service {}

Interfaces

Single-method interfaces are named with the method name plus the -er suffix:

// ✅ Single-method — method name + er
type Reader interface {
    Read(p []byte) (n int, err error)
}
type Writer interface {
    Write(p []byte) (n int, err error)
}
type OrderFetcher interface {
    FetchOrder(ctx context.Context, id int64) (*Order, error)
}

// ✅ Multi-method — descriptive noun
type OrderRepository interface {
    FindByID(ctx context.Context, id int64) (*Order, error)
    Save(ctx context.Context, order *Order) error
    Delete(ctx context.Context, id int64) error
}

Keep interfaces small

A two-method interface is easier to implement, mock, and satisfy than a ten-method one. If an interface is growing large, break it into smaller, composable pieces.

Functions and Methods

Functions that return something use noun-like names. Functions that do something use verb-like names:

// ✅ Returns something — noun-like
func NewOrderService(repo OrderRepository) *OrderService {}
func ParseURL(raw string) (*url.URL, error) {}
func (o *Order) Total() Money {}

// ✅ Does something — verb-like
func (s *OrderService) ProcessPayment(ctx context.Context, p *Payment) error {}
func (o *Order) Cancel() error {}

Avoid the Get Prefix

In Go, the Get prefix is not used for accessor methods:

// ✅ Go style — no Get prefix
func (o *Order) Status() OrderStatus { return o.status }
func (u *User) Email() string        { return u.email }

// ❌ Not Go style
func (o *Order) GetStatus() OrderStatus { return o.status }

Constructors

Go uses the New prefix for constructor functions:

// ✅ Constructor validates and returns ready-to-use value
func NewOrderService(repo OrderRepository, logger *slog.Logger) (*OrderService, error) {
    if repo == nil {
        return nil, errors.New("repo must not be nil")
    }
    return &OrderService{repo: repo, logger: logger}, nil
}

Variables

Go favours short variable names for short-lived, locally scoped variables. Name length should be proportional to scope:

// ✅ Short names for short-lived variables
for i, v := range items {
    process(v)
}

// ✅ Descriptive names for wider scope
func processOrders(ctx context.Context, orders []*Order) error {
    for _, order := range orders {
        if err := validateOrder(order); err != nil {
            return fmt.Errorf("validating order %d: %w", order.ID, err)
        }
    }
    return nil
}

Common idiomatic short names:

Variable Meaning
i, j, k Loop indices
n Count or number
v Value (in range loops)
k Key (in map range loops)
r, w io.Reader, io.Writer
ctx context.Context — always ctx
err Error return value — always err
buf Buffer
b []byte slice
mu sync.Mutex
wg sync.WaitGroup

Constants and iota

Constants follow the same MixedCaps / mixedCaps rule. Use iota for enumerations:

// ✅ Exported constants — MixedCaps
const (
    MaxRetryCount   = 3
    DefaultTimeout  = 30 * time.Second
    DefaultPageSize = 20
)

// ✅ iota for ordered enumerations
type OrderStatus int

const (
    StatusPending   OrderStatus = iota // 0
    StatusConfirmed                    // 1
    StatusShipped                      // 2
    StatusDelivered                    // 3
    StatusCancelled                    // 4
)

// ✅ iota with bit shifts for flags
type Permission uint

const (
    PermRead    Permission = 1 << iota // 1
    PermWrite                          // 2
    PermExecute                        // 4
    PermAdmin                          // 8
)

// ✅ iota with a typed string constant — use string constants instead of iota
// when the value must be human-readable (e.g. JSON, database)
type OrderStatusStr string

const (
    StatusPendingStr   OrderStatusStr = "pending"
    StatusConfirmedStr OrderStatusStr = "confirmed"
    StatusCancelledStr OrderStatusStr = "cancelled"
)

// ❌ UPPER_SNAKE — not Go convention
const MAX_RETRY_COUNT = 3

Error Variables

Sentinel errors use the Err prefix. Custom error types use the Error suffix:

// ✅ Sentinel errors — Err prefix
var (
    ErrNotFound       = errors.New("not found")
    ErrUnauthorised   = errors.New("unauthorised")
    ErrInvalidInput   = errors.New("invalid input")
    ErrAlreadyExists  = errors.New("already exists")
)

// ✅ Custom error type — Error suffix
type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation failed on %q: %s", e.Field, e.Message)
}

type NotFoundError struct {
    Resource string
    ID       int64
}

func (e *NotFoundError) Error() string {
    return fmt.Sprintf("%s with id %d not found", e.Resource, e.ID)
}

Method Receivers

Receiver names must be short — typically one or two letters abbreviated from the type name. Use the same receiver name consistently across all methods of a type:

// ✅ Short, consistent receiver name
type OrderService struct { ... }

func (s *OrderService) GetOrder(ctx context.Context, id int64) (*Order, error)   {}
func (s *OrderService) CreateOrder(ctx context.Context, req *CreateOrderRequest) (*Order, error) {}
func (s *OrderService) CancelOrder(ctx context.Context, id int64) error          {}

// ❌ Verbose receiver names — not Go idiom
func (orderService *OrderService) GetOrder(...) {}
func (this *OrderService) GetOrder(...) {}    // 'this' is not Go idiom
func (self *OrderService) GetOrder(...) {}    // 'self' is not Go idiom

this and self are not Go

Using this or self signals unfamiliarity with Go to reviewers. The convention is a short abbreviation of the type name.

Type Aliases and Defined Types

Use defined types (type X Y) to add methods or domain meaning. Use type aliases (type X = Y) only for genuine aliasing — migration, bridging packages:

// ✅ Defined type — adds domain meaning and allows methods
type UserID int64
type OrderStatus string
type Money struct {
    Amount   decimal.Decimal
    Currency string
}

// Defined types can have methods
func (id UserID) IsValid() bool { return id > 0 }
func (s OrderStatus) IsTerminal() bool {
    return s == StatusDelivered || s == StatusCancelled
}

// ✅ Type alias — only when bridging two packages or for migration
type LegacyOrderID = int64  // alias: LegacyOrderID IS int64, not a new type

// ❌ Using type alias where a defined type would add clarity
type UserID = int64  // just use int64 if you're not adding methods or constraints

Embedded Types

When embedding a type to promote its methods, do not name the embedded field — the type name serves as the field name:

// ✅ Embedded type — promotes all exported methods of the embedded type
type TimestampedEntity struct {
    CreatedAt time.Time
    UpdatedAt time.Time
}

type Order struct {
    TimestampedEntity          // embedded — Order gains CreatedAt and UpdatedAt
    ID     int64
    UserID int64
    Status OrderStatus
}

// Access via the outer struct directly:
order.CreatedAt  // promoted field
order.UpdatedAt  // promoted field

// ✅ Embedding an interface in a struct (for test doubles)
type MockOrderRepository struct {
    OrderRepository             // embed interface — panic on unimplemented methods
    findByIDFunc func(ctx context.Context, id int64) (*Order, error)
}

func (m *MockOrderRepository) FindByID(ctx context.Context, id int64) (*Order, error) {
    if m.findByIDFunc != nil {
        return m.findByIDFunc(ctx, id)
    }
    return m.OrderRepository.FindByID(ctx, id)
}

Test Function Naming

Go test functions use the pattern Test<FunctionName> for simple tests and Test<FunctionName>_<Scenario> for table-driven or scenario-specific tests:

// ✅ Simple test
func TestCalculateTotal(t *testing.T) {}

// ✅ Scenario-specific tests
func TestCreateOrder_WhenCartIsEmpty_ReturnsError(t *testing.T) {}
func TestCreateOrder_WithValidItems_ReturnsOrder(t *testing.T) {}
func TestGetOrder_WhenNotFound_ReturnsErrNotFound(t *testing.T) {}

// ✅ Benchmark
func BenchmarkCalculateTotal(b *testing.B) {
    for i := 0; i < b.N; i++ {
        calculateTotal(items)
    }
}

// ✅ Example — appears in godoc
func ExampleOrderService_CreateOrder() {
    svc := NewOrderService(repo, logger)
    order, err := svc.CreateOrder(ctx, req)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(order.Status)
    // Output: pending
}

File Names

Go source files use snake_case (lowercase with underscores):

✅ Correct file names
order_service.go
http_client.go
payment_processor.go
order_service_test.go    ← test files always end in _test.go
Suffix Purpose
_test.go Test file — compiled only during go test
_linux.go Build constraint — Linux only
_windows.go Build constraint — Windows only
_amd64.go Build constraint — AMD64 architecture only