Craft ⏱ 6 min read

04 · Best Practices

Naming, interfaces, composition, table-driven tests

Naming: Exported vs Unexported

Capital first letter = exported (public). Lowercase = unexported (package-private).

// Exported — visible outside the package
type CouponService struct { ... }
func NewCouponService(...) *CouponService { ... }

// Unexported — internal to the package
type couponValidator struct { ... }
func validateStatus(s string) bool { ... }

Keep identifiers short and clear. In a loop, i beats index. For a method receiver, use a 1-2 letter abbreviation of the type: c *CouponService not cs *CouponService — actually use s for service, h for handler, r for repo.

Small Interfaces

Define interfaces where they are consumed, not where they are implemented. Keep them small.

// Good — small, focused interface
type CouponGetter interface {
  GetByID(ctx context.Context, id string) (*models.Coupon, error)
}

// Bad — huge interface forces consumers to depend on methods they don't use
type CouponRepository interface {
  GetByID(...)
  GetAll(...)
  Create(...)
  Update(...)
  Delete(...)
  // ... 15 more methods
}

The standard library's io.Reader (1 method) and io.Writer (1 method) are the gold standard for interface design.

Accept Interfaces, Return Structs

// Good — accepts an interface (easy to test with mocks)
func NewCouponService(repo repository.CouponRepository, ...) *CouponService {
  return &CouponService{repo: repo}
}

// Bad — accepts a concrete type (hard to test, tight coupling)
func NewCouponService(repo *repository.GORMCouponRepository, ...) *CouponService { ... }

Return concrete structs from constructors. Callers that need to mock can refer to the interface.

Composition Over Embedding

Go has no inheritance. Embed structs to compose behavior.

type BaseService struct {
  logger zlog.Logger
}

type CouponService struct {
  BaseService               // embed — CouponService gets logger field
  repo repository.CouponRepository
}

// Usage: s.logger.Info(...) — logger is promoted

Table-Driven Tests

Group related test cases in a slice of structs, then loop over them.

func TestGenerateCouponInternalName(t *testing.T) {
  tests := []struct {
    input    string
    expected string
  }{
    {"Welcome Voucher 100 Baht", "WELCOME_VOUCHER_100_BAHT"},
    {"simple",                   "SIMPLE"},
    {"  trimmed  ",              "TRIMMED"},
    {"",                         ""},
  }

  for _, tt := range tests {
    result := generateCouponInternalName(tt.input)
    assert.Equal(t, tt.expected, result, "input: %q", tt.input)
  }
}

This pattern is from lp-coupon-api/internal/services/coupon_service_test.go. Add a name string field to the struct for clearer test output on failure.

Key Takeaways