Go接口設(shè)計與依賴注入的實踐方法
前言
Go的接口是隱式實現(xiàn)的,不需要顯式聲明"實現(xiàn)了某個接口"。這種設(shè)計讓代碼解耦更自然,但也容易寫出難以測試和維護(hù)的代碼。
本文整理接口設(shè)計的常見模式和依賴注入的實踐方法,目標(biāo)是寫出更易測試、更靈活的Go代碼。
1. 接口設(shè)計原則
1.1 接口要小
Go社區(qū)有句話:接口越小越好。標(biāo)準(zhǔn)庫里大量的接口只有一兩個方法。
// io包里的經(jīng)典小接口
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
// 組合成更大的接口
type ReadWriter interface {
Reader
Writer
}
type ReadWriteCloser interface {
Reader
Writer
Closer
}
為什么小接口更好:
- 實現(xiàn)成本低,更多類型可以滿足
- 組合靈活,按需組合
- Mock簡單,測試容易
1.2 在使用方定義接口
不要在實現(xiàn)方定義接口,而是在使用方按需定義。
// ? 不推薦:在實現(xiàn)方定義大接口
package user
type UserService interface {
GetUser(id int) (*User, error)
CreateUser(u *User) error
UpdateUser(u *User) error
DeleteUser(id int) error
ListUsers(page, size int) ([]*User, error)
SearchUsers(query string) ([]*User, error)
// ... 更多方法
}
type userServiceImpl struct {
db *sql.DB
}
func NewUserService(db *sql.DB) UserService {
return &userServiceImpl{db: db}
}
// ? 推薦:在使用方按需定義小接口
package order
// 只定義需要的方法
type UserGetter interface {
GetUser(id int) (*user.User, error)
}
type OrderService struct {
users UserGetter // 依賴接口,不是具體實現(xiàn)
}
func (s *OrderService) CreateOrder(userID int, items []Item) error {
u, err := s.users.GetUser(userID)
if err != nil {
return err
}
// 創(chuàng)建訂單邏輯
}
1.3 接受接口,返回結(jié)構(gòu)體
// ? 推薦
func NewServer(logger Logger, db Database) *Server {
return &Server{
logger: logger,
db: db,
}
}
// ? 不推薦
func NewServer(logger Logger, db Database) ServerInterface {
return &Server{
logger: logger,
db: db,
}
}
原因:
- 返回具體類型,調(diào)用方可以訪問所有方法
- 返回接口會限制調(diào)用方,失去靈活性
- 接口應(yīng)該在使用方定義,不是實現(xiàn)方
2. 常見接口設(shè)計模式
2.1 策略模式
不同的算法封裝成接口,運行時切換。
// 定義策略接口
type Compressor interface {
Compress(data []byte) ([]byte, error)
Decompress(data []byte) ([]byte, error)
}
// Gzip實現(xiàn)
type GzipCompressor struct{}
func (g *GzipCompressor) Compress(data []byte) ([]byte, error) {
var buf bytes.Buffer
w := gzip.NewWriter(&buf)
if _, err := w.Write(data); err != nil {
return nil, err
}
if err := w.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func (g *GzipCompressor) Decompress(data []byte) ([]byte, error) {
r, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
return nil, err
}
defer r.Close()
return io.ReadAll(r)
}
// Zstd實現(xiàn)
type ZstdCompressor struct{}
func (z *ZstdCompressor) Compress(data []byte) ([]byte, error) {
// zstd壓縮實現(xiàn)
}
func (z *ZstdCompressor) Decompress(data []byte) ([]byte, error) {
// zstd解壓實現(xiàn)
}
// 使用方
type FileStorage struct {
compressor Compressor
}
func (s *FileStorage) Save(filename string, data []byte) error {
compressed, err := s.compressor.Compress(data)
if err != nil {
return err
}
return os.WriteFile(filename, compressed, 0644)
}
2.2 裝飾器模式
在不修改原有實現(xiàn)的情況下增加功能。
// 基礎(chǔ)接口
type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}
// 標(biāo)準(zhǔn)庫的http.Client滿足這個接口
var _ HTTPClient = (*http.Client)(nil)
// 帶日志的裝飾器
type LoggingClient struct {
client HTTPClient
logger Logger
}
func (c *LoggingClient) Do(req *http.Request) (*http.Response, error) {
start := time.Now()
c.logger.Info("request", "method", req.Method, "url", req.URL.String())
resp, err := c.client.Do(req)
c.logger.Info("response",
"method", req.Method,
"url", req.URL.String(),
"duration", time.Since(start),
"status", resp.StatusCode,
)
return resp, err
}
// 帶重試的裝飾器
type RetryClient struct {
client HTTPClient
maxRetries int
backoff time.Duration
}
func (c *RetryClient) Do(req *http.Request) (*http.Response, error) {
var lastErr error
for i := 0; i <= c.maxRetries; i++ {
resp, err := c.client.Do(req)
if err == nil && resp.StatusCode < 500 {
return resp, nil
}
lastErr = err
time.Sleep(c.backoff * time.Duration(i+1))
}
return nil, fmt.Errorf("max retries exceeded: %w", lastErr)
}
// 組合使用
func NewHTTPClient() HTTPClient {
base := &http.Client{Timeout: 10 * time.Second}
withRetry := &RetryClient{client: base, maxRetries: 3, backoff: time.Second}
withLogging := &LoggingClient{client: withRetry, logger: defaultLogger}
return withLogging
}
2.3 適配器模式
將不兼容的接口轉(zhuǎn)換為期望的接口。
// 目標(biāo)接口
type Cache interface {
Get(key string) ([]byte, error)
Set(key string, value []byte, ttl time.Duration) error
Delete(key string) error
}
// Redis客戶端(第三方庫的接口)
type RedisClient struct {
// ...
}
func (r *RedisClient) GetString(key string) (string, error) { ... }
func (r *RedisClient) SetEx(key string, value string, seconds int) error { ... }
func (r *RedisClient) Del(keys ...string) error { ... }
// 適配器
type RedisAdapter struct {
client *RedisClient
}
func (a *RedisAdapter) Get(key string) ([]byte, error) {
s, err := a.client.GetString(key)
if err != nil {
return nil, err
}
return []byte(s), nil
}
func (a *RedisAdapter) Set(key string, value []byte, ttl time.Duration) error {
return a.client.SetEx(key, string(value), int(ttl.Seconds()))
}
func (a *RedisAdapter) Delete(key string) error {
return a.client.Del(key)
}
// 使用
var _ Cache = (*RedisAdapter)(nil)
3. 依賴注入
3.1 構(gòu)造函數(shù)注入
最常用的方式,依賴通過構(gòu)造函數(shù)傳入。
// 定義依賴接口
type Logger interface {
Info(msg string, args ...interface{})
Error(msg string, args ...interface{})
}
type UserRepository interface {
GetByID(id int) (*User, error)
Save(u *User) error
}
type EmailSender interface {
Send(to, subject, body string) error
}
// 服務(wù)定義
type UserService struct {
logger Logger
repo UserRepository
email EmailSender
}
// 構(gòu)造函數(shù)注入
func NewUserService(logger Logger, repo UserRepository, email EmailSender) *UserService {
return &UserService{
logger: logger,
repo: repo,
email: email,
}
}
func (s *UserService) Register(email, password string) (*User, error) {
s.logger.Info("registering user", "email", email)
user := &User{
Email: email,
Password: hashPassword(password),
}
if err := s.repo.Save(user); err != nil {
s.logger.Error("failed to save user", "error", err)
return nil, err
}
if err := s.email.Send(email, "Welcome", "Welcome to our service!"); err != nil {
s.logger.Error("failed to send welcome email", "error", err)
// 不返回錯誤,發(fā)郵件失敗不影響注冊
}
return user, nil
}
3.2 函數(shù)選項模式
適合有多個可選依賴的情況。
type Server struct {
addr string
logger Logger
db Database
cache Cache
timeout time.Duration
}
type Option func(*Server)
func WithLogger(l Logger) Option {
return func(s *Server) {
s.logger = l
}
}
func WithDatabase(db Database) Option {
return func(s *Server) {
s.db = db
}
}
func WithCache(c Cache) Option {
return func(s *Server) {
s.cache = c
}
}
func WithTimeout(d time.Duration) Option {
return func(s *Server) {
s.timeout = d
}
}
func NewServer(addr string, opts ...Option) *Server {
s := &Server{
addr: addr,
logger: defaultLogger, // 默認(rèn)值
timeout: 30 * time.Second,
}
for _, opt := range opts {
opt(s)
}
return s
}
// 使用
server := NewServer(":8080",
WithLogger(customLogger),
WithDatabase(mysqlDB),
WithTimeout(60*time.Second),
)
3.3 手動組裝依賴
對于小型項目,在main函數(shù)里手動組裝足夠。
func main() {
// 基礎(chǔ)設(shè)施層
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
db, err := sql.Open("mysql", os.Getenv("DATABASE_URL"))
if err != nil {
log.Fatal(err)
}
defer db.Close()
redisClient := redis.NewClient(&redis.Options{
Addr: os.Getenv("REDIS_URL"),
})
// 倉儲層
userRepo := repository.NewUserRepository(db)
orderRepo := repository.NewOrderRepository(db)
// 服務(wù)層
emailSender := email.NewSMTPSender(os.Getenv("SMTP_HOST"))
cache := cache.NewRedisCache(redisClient)
userService := service.NewUserService(logger, userRepo, emailSender)
orderService := service.NewOrderService(logger, orderRepo, userRepo, cache)
// HTTP層
userHandler := handler.NewUserHandler(userService)
orderHandler := handler.NewOrderHandler(orderService)
// 路由
mux := http.NewServeMux()
mux.HandleFunc("/users", userHandler.Handle)
mux.HandleFunc("/orders", orderHandler.Handle)
// 啟動
server := &http.Server{
Addr: ":8080",
Handler: mux,
}
log.Fatal(server.ListenAndServe())
}
3.4 使用Wire做依賴注入
項目大了之后,手動組裝依賴很繁瑣。Wire是Google出的依賴注入代碼生成工具。
// wire.go
//go:build wireinject
// +build wireinject
package main
import (
"github.com/google/wire"
)
func InitializeApp() (*App, error) {
wire.Build(
// 提供者
NewConfig,
NewLogger,
NewDatabase,
NewRedisClient,
// 倉儲
repository.NewUserRepository,
repository.NewOrderRepository,
// 服務(wù)
service.NewUserService,
service.NewOrderService,
// Handler
handler.NewUserHandler,
handler.NewOrderHandler,
// App
NewApp,
)
return nil, nil
}
運行wire命令,自動生成組裝代碼。
4. 測試中的接口使用
4.1 手寫Mock
簡單場景下,手寫Mock最直接。
// 生產(chǎn)代碼
type UserRepository interface {
GetByID(id int) (*User, error)
Save(u *User) error
}
// 測試用Mock
type MockUserRepository struct {
GetByIDFunc func(id int) (*User, error)
SaveFunc func(u *User) error
}
func (m *MockUserRepository) GetByID(id int) (*User, error) {
if m.GetByIDFunc != nil {
return m.GetByIDFunc(id)
}
return nil, nil
}
func (m *MockUserRepository) Save(u *User) error {
if m.SaveFunc != nil {
return m.SaveFunc(u)
}
return nil
}
// 測試
func TestUserService_Register(t *testing.T) {
mockRepo := &MockUserRepository{
SaveFunc: func(u *User) error {
if u.Email == "" {
return errors.New("email required")
}
u.ID = 1
return nil
},
}
mockEmail := &MockEmailSender{
SendFunc: func(to, subject, body string) error {
return nil
},
}
svc := NewUserService(testLogger, mockRepo, mockEmail)
user, err := svc.Register("test@example.com", "password")
assert.NoError(t, err)
assert.Equal(t, 1, user.ID)
}
4.2 使用mockgen
項目大了用mockgen自動生成Mock代碼。
# 安裝 go install go.uber.org/mock/mockgen@latest # 生成 mockgen -source=repository.go -destination=mock_repository.go -package=repository
// 使用生成的Mock
func TestUserService_Register(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRepo := NewMockUserRepository(ctrl)
mockEmail := NewMockEmailSender(ctrl)
// 設(shè)置期望
mockRepo.EXPECT().
Save(gomock.Any()).
DoAndReturn(func(u *User) error {
u.ID = 1
return nil
})
mockEmail.EXPECT().
Send("test@example.com", gomock.Any(), gomock.Any()).
Return(nil)
svc := NewUserService(testLogger, mockRepo, mockEmail)
user, err := svc.Register("test@example.com", "password")
assert.NoError(t, err)
assert.Equal(t, 1, user.ID)
}
4.3 表驅(qū)動測試
配合接口,表驅(qū)動測試很方便。
func TestUserService_GetUser(t *testing.T) {
tests := []struct {
name string
userID int
setup func(*MockUserRepository)
want *User
wantErr bool
}{
{
name: "user exists",
userID: 1,
setup: func(m *MockUserRepository) {
m.GetByIDFunc = func(id int) (*User, error) {
return &User{ID: 1, Email: "test@example.com"}, nil
}
},
want: &User{ID: 1, Email: "test@example.com"},
},
{
name: "user not found",
userID: 999,
setup: func(m *MockUserRepository) {
m.GetByIDFunc = func(id int) (*User, error) {
return nil, ErrNotFound
}
},
wantErr: true,
},
{
name: "database error",
userID: 1,
setup: func(m *MockUserRepository) {
m.GetByIDFunc = func(id int) (*User, error) {
return nil, errors.New("connection refused")
}
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockRepo := &MockUserRepository{}
tt.setup(mockRepo)
svc := NewUserService(testLogger, mockRepo, nil)
got, err := svc.GetUser(tt.userID)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}
5. 實際項目結(jié)構(gòu)
一個典型的分層結(jié)構(gòu):
myapp/ ├── cmd/ │ └── server/ │ └── main.go # 組裝依賴,啟動服務(wù) ├── internal/ │ ├── domain/ # 領(lǐng)域模型 │ │ ├── user.go │ │ └── order.go │ ├── repository/ # 數(shù)據(jù)訪問層 │ │ ├── interface.go # 接口定義 │ │ ├── user.go # 實現(xiàn) │ │ └── user_test.go │ ├── service/ # 業(yè)務(wù)邏輯層 │ │ ├── user.go │ │ └── user_test.go │ └── handler/ # HTTP處理層 │ ├── user.go │ └── user_test.go ├── pkg/ # 可復(fù)用的庫 │ ├── logger/ │ └── cache/ └── go.mod
接口定義位置的選擇:
// internal/repository/interface.go
// 倉儲接口定義在repository包,因為實現(xiàn)也在這里
type UserRepository interface {
GetByID(id int) (*domain.User, error)
Save(u *domain.User) error
// ...
}
// internal/service/user.go
// 服務(wù)層依賴接口,但接口可能定義在其他包
type UserService struct {
repo repository.UserRepository // 或者在service包里定義小接口
logger Logger
}
總結(jié)
| 要點 | 說明 |
|---|---|
| 接口要小 | 一兩個方法最好,便于實現(xiàn)和Mock |
| 在使用方定義接口 | 按需定義,避免大而全的接口 |
| 接受接口返回結(jié)構(gòu)體 | 構(gòu)造函數(shù)接收接口類型,返回具體類型 |
| 構(gòu)造函數(shù)注入 | 最常用的依賴注入方式 |
| 函數(shù)選項模式 | 適合可選依賴多的情況 |
| Mock測試 | 小項目手寫Mock,大項目用mockgen |
| 分層結(jié)構(gòu) | domain/repository/service/handler |
核心思想:
- 面向接口編程,但不要過度設(shè)計。不是所有東西都要抽接口,有明確需求再抽。
- 依賴倒置。高層模塊不依賴低層模塊,都依賴抽象。
- 可測試性。寫代碼時想著怎么測試,自然會寫出松耦合的代碼。
接口設(shè)計沒有銀彈,關(guān)鍵是理解原則背后的原因,然后根據(jù)項目實際情況做取舍。
到此這篇關(guān)于Go接口設(shè)計與依賴注入的實踐方法的文章就介紹到這了,更多相關(guān)Go接口設(shè)計與依賴注入內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
gin自定義中間件解決requestBody不可重讀(請求體取值)
這篇文章主要介紹了gin自定義中間件解決requestBody不可重讀,確保控制器能夠獲取請求體值,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10

