Using Structures and Interfaces
Struct
Naming, avoid redundant / stuttering exported struct names with the package name.
package auth
// ! BAD - don't do this, when used from another package it will be
// `auth.AuthService{}`
type AuthService struct { }
type AuthConfig struct { } // ==> auth.AuthConfig
// ! Good, when used from another package it becomes `auth.Service{}`
type Service struct{}
type Config struct { } // ==> auth.ConfigConstructor, with the format
to be exported (called from outside package)
New<Struct Name>(<dependency list>)
for internal (private constructor)
new<struct name>(<dependency list>)
// NewService returns new service for given postgres database
func NewService(db *sql.DB) *Service {
return &Service{
db: db,
}
}
func newUserProductResponse(p postgres.TbProductRow) UserProductResponse {
var response UserProductResponse
response.ID = p.ID
...
return response
}
Dependency, for structs that have methods, external its dependencies (both interface, config, db, redis, httpClient) are injected via constructor.
// BAD
type Service struct{}
const key := "my_static_key"
// * methods are more difficult to unit test, because of their dependencies
// bound to external package
// * code is more difficult to read, because you have to look at implementation details
// to see dependencies2 used
func (s *Service) CreateResource(ctx context.Context, r *Resource) error {
// get global db variable
db := configdb.GetDB()
db.Query...
// get global value by config
configVal := configStore.GetVal(key)
if configVal == ... {
....
}
// get redis variables
rClient := configRedis.GetRedis()
rCient.Set(..)
...
return nil
}
// GOOD
type Service struct {
db *sql.DB
rClient *redis.Client
configStore config.KVStore
}
func NewService(db *sql.DB, r *redis.Client, kv config.KVStore) *Service {
return &Service{
db: db,
rClient: r,
configStore: kv,
}
}
func (s *Service) CreateResource(ctx context.Context, r *Resource) error {
s.db.Query...
// get global value by config
configVal := s.configStore.GetVal(key)
if configVal == ... {
....
}
// get redis variables
s.rClient.Set(..)
...
return nil
}
Interfaces
When to use the interface?
- When we need a method, regardless of implementation and there is a possibility that implementation is more than 1.
- When you need mocking functionality during unit tests.
- If it turns out that in our program there is only an interface declaration with 1 implementation, it's better to use struct/pointer-struct with usual methods.
Naming, use a name that reflects the behavior of the interface.
package io
type Reader interface {
Read(...)
}
// =======================
promotional packages
type Processor interface {
Process(...)
}Define interface in the package consumer side (packages that use interfaces tb)
// BAD, interface and implementation defined in the same package
package auth
type Validator interface {
Validate(ctx context.Context, data Data) error
}
type ValidatorImpl struct {
...
}
func (v *ValidatorImpl) Validate(ctx context.Context, data Data) error {
....
}
// GOOD,
package controller
type validator interface {
Validate(ctx context.Context, data Data) error
}
func createUser(v validator) echo.HandlerFunc {
return func(ctx echo.Context) error {
data := ctx.Param(...)
// use the interface method
if err := v.Validate(data); err != nil {
...
}
...
}
}
//================
package auth
type Validator struct { }
func (v *Validator) Validate(ctx context.Context, data Data) error {If consumed by multiple packages, then define in the general package
package service
type Validator interface {
Validate(ctx context.Context, data Data) error
}
// =======================
package grpc
type Server struct {
v service.Validator
}
func (s *Server) CreateUser(ctx context.Context, req Request) (Response, error) {
if err := s.v.Validate(...); err != nil {
...
}
...
}
// ======================
package controller
func createUser(v service.Vali