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
- Exported = capital. Keep names short and clear — abbreviate receiver names (s, h, r)
- Define interfaces where consumed, not where implemented. Keep them small (1-3 methods)
- Constructor functions accept interfaces, return concrete structs
- Compose behavior via embedding — no inheritance in Go
- Table-driven tests:
[]struct{input, want}loop — clean and extensible