当前位置: 首页 > article >正文

【go语言规范】关于接口设计

抽象应该被发现,而不是被创造。为了避免不必要的复杂性,需要时才创建接口,而不是预见到需要它,或者至少可以证明这种抽象是有价值的。
“The bigger the interface, the weaker the abstraction.
不要用接口进行设计,要发现他们
——rob pike
作为一个常用java的程序员,在创建具体类型之前创建接口是很自然的,但是go不应该这样工作。
创建接口是为了创建抽象。当编程时遇到抽象,主要的注意事项时记住应该发现抽象,而不是创建抽象。
这意味着如果没有直接的理由,不应该在代码中创建抽象。
我们不应该用接口来设计,而应该等待具体的需求。
应该在需要的时候创建一个接口,而不是预见到可能需要他的时候。
如果没有充足的理由添加接口,也不清楚接口如何使代码变得更好,那么我们应该质疑这个接口的目的。为什么不直接调用实现呢?
哲学: 不要试图抽象地解决问题,而适应解决现在必须解决的问题。
如果不清楚接口如何是代码变得更好,我们应该考虑删除它,以使代码更简单。
哲学的哲学:不要抽象,干实事。
Go 语言接口设计原则

  1. 接口要小而精准
    单一职责原则
    // 不推荐 - 接口过大
    type Storage interface {
    Save(data interface{}) error
    Delete(id string) error
    Update(data interface{}) error
    Get(id string) interface{}
    List() []interface{}
    }
    // 推荐 - 拆分成小接口
    type Writer interface {
    Save(data interface{}) error
    }
    type Reader interface {
    Get(id string) interface{}
    }

  2. 以使用者为中心
    接口应该在使用处定义
    type CustomerService struct {
    // 在需要的地方定义接口
    store customerStorer
    }
    // 最小化接口定义
    type customerStorer interface {
    Store(Customer) error
    }

  3. 组合优于继承
    通过小接口组合
    type Reader interface {
    Read(p []byte) (n int, err error)
    }
    type Writer interface {
    Write(p []byte) (n int, err error)
    }
    // 组合接口
    type ReadWriter interface {
    Reader
    Writer
    }

  4. 遵循 Go 的惯例
    使用动词+er命名
    type Stringer interface {
    String() string
    }
    type Handler interface {
    Handle(msg Message) error
    }

  5. 接口隔离原则
    客户端不应依赖它不使用的方法
    // 不推荐
    type FileProcessor interface {
    Open(name string) error
    Read(b []byte) (n int, err error)
    Write(b []byte) (n int, err error)
    Close() error
    }
    // 推荐
    type FileReader interface {
    Read(b []byte) (n int, err error)
    }
    type FileWriter interface {
    Write(b []byte) (n int, err error)
    }

  6. 最佳实践

  7. 保持接口小巧
    // 好的实践
    type Logger interface {
    Log(message string) error
    }
    // 而不是
    type Logger interface {
    Log(message string) error
    Logf(format string, args …interface{}) error
    Debug(message string)
    Error(message string)
    // … 更多方法
    }

  8. 使用抽象接口
    // 具体的实现
    type Customer struct {
    ID string
    Name string
    }
    // 抽象的接口
    type CustomerFinder interface {
    FindCustomer(id string) (*Customer, error)
    }

  9. 避免接口污染
    // 不推荐
    type Config interface {
    // 过多的方法导致实现困难
    Database() string
    APIKey() string
    Timeout() time.Duration
    MaxRetries() int
    CacheSize() int
    // …更多配置项
    }
    // 推荐
    type DatabaseConfig interface {
    Database() string
    }
    type APIConfig interface {
    APIKey() string
    }

  10. 注意事项
    显式优于隐式
    Go 的接口是隐式实现的
    但应该在文档中明确说明接口的期望行为
    面向接口编程
    依赖接口而不是具体实现
    便于测试和模拟
    避免过度设计
    不要为了接口而接口
    从实际需求出发设计接口
    注意性能影响
    接口调用会有少量性能开销
    在性能关键路径上要谨慎使用
    以上原则可以帮助我们设计出更好的 Go 接口,使代码更加灵活、可维护和可测试。

什么是接口污染?我们如何避免接口污染?
接口污染及其避免方法
接口污染(Interface Pollution)是指创建了不必要的或过大的接口,导致代码复杂性增加和维护困难的情况。
接口污染的表现形式

  1. 过大的接口
    // 不好的示例 - 接口污染
    type Storage interface {
    CreateCustomer(Customer) error
    UpdateCustomer(Customer) error
    DeleteCustomer(string) error
    GetCustomer(string) (Customer, error)
    ListCustomers() ([]Customer, error)
    CreateOrder(Order) error
    UpdateOrder(Order) error
    DeleteOrder(string) error
    // … 更多方法
    }

  2. 不必要的抽象
    // 不好的示例 - 过度抽象
    type StringFormatter interface {
    Format(string) string
    }
    // 这种简单操作不需要接口
    func ProcessString(formatter StringFormatter, s string) string {
    return formatter.Format(s)
    }

如何避免接口污染

  1. 遵循接口隔离原则
    // 好的示例 - 小而专注的接口
    type CustomerReader interface {
    GetCustomer(id string) (Customer, error)
    }
    type CustomerWriter interface {
    CreateCustomer(Customer) error
    UpdateCustomer(Customer) error
    }
    // 如果需要同时读写,可以组合接口
    type CustomerStorage interface {
    CustomerReader
    CustomerWriter
    }

  2. 在使用处定义接口
    // 好的示例 - 接口在使用处定义
    type CustomerService struct {
    // 只定义需要的方法
    store customerStorer
    }
    type customerStorer interface {
    StoreCustomer(Customer) error
    }

  3. 保持接口小巧精简
    // 好的示例 - 最小化接口
    type Logger interface {
    Log(message string) error
    }
    // 具体实现可以有更多方法
    type FileLogger struct {
    path string
    }
    func (f *FileLogger) Log(message string) error {
    // 实现日志记录
    return nil
    }
    func (f *FileLogger) Close() error {
    // 额外的方法
    return nil
    }

  4. 根据实际需求设计
    // 好的示例 - 基于实际需求的接口
    type PaymentProcessor interface {
    Process(payment Payment) error
    }
    // 不同的支付方式实现这个接口
    type StripePayment struct{}
    type PayPalPayment struct{}

接口设计最佳实践
单一职责
每个接口应该只表达一个清晰的概念
避免创建"万能"接口
显式依赖
明确声明所需的依赖
避免隐式依赖
基于需求驱动
等到真正需要时才创建接口
避免过度设计
持续重构
定期审查接口设计
及时拆分过大的接口
示例:重构污染的接口
// 原始污染的接口
type UserService interface {
CreateUser(User) error
UpdateUser(User) error
DeleteUser(string) error
GetUser(string) (User, error)
AuthenticateUser(string, string) bool
ResetPassword(string) error
}
// 重构后的小接口
type UserCreator interface {
CreateUser(User) error
}
type UserUpdater interface {
UpdateUser(User) error
}
type UserAuthenticator interface {
AuthenticateUser(string, string) bool
}
// 根据实际需求组合接口
type UserManager struct {
creator UserCreator
auth UserAuthenticator
}

通过以上方法,我们可以有效避免接口污染,使代码更加清晰、可维护和可测试。

我们不应该在生产端创建接口,同时创建具体的实现。这是c#和 Java的习惯 。

由于应该发现抽象而不是创建抽象,这意味着生产者不能强制所有客户端使用给定的抽象,相反,有客户来决定他是否需要某种形式的抽象,然后为他的需求确定最佳的抽象级别。

在 Go 中平衡抽象与具体实现

  1. 遵循 YAGNI 原则(You Ain’t Gonna Need It)
    不好的做法
    // 过度抽象的接口
    type CustomerStorage interface {
    StoreCustomer(customer Customer) error
    GetCustomer(id string) (Customer, error)
    UpdateCustomer(customer Customer) error
    DeleteCustomer(id string) error
    GetAllCustomers() ([]Customer, error)
    GetCustomersWithoutContract() ([]Customer, error)
    GetCustomersWithNegativeBalance() ([]Customer, error)
    // 可能用不到的方法…
    }

好的做法
// 根据实际需求定义小接口
type CustomerReader interface {
GetCustomer(id string) (Customer, error)
}
type CustomerWriter interface {
StoreCustomer(customer Customer) error
}

  1. 使用组合而不是继承
    // 基础接口
    type Reader interface {
    Read(p []byte) (n int, err error)
    }
    type Writer interface {
    Write(p []byte) (n int, err error)
    }
    // 按需组合
    type ReadWriter interface {
    Reader
    Writer
    }

  2. 在使用处定义接口
    type CustomerService struct {
    // 只定义服务真正需要的方法
    store interface {
    GetCustomer(id string) (Customer, error)
    StoreCustomer(Customer) error
    }
    }

  3. 使用适配器模式保持代码整洁
    // 具体实现
    type PostgresDB struct {
    // …
    }
    // 适配器
    type CustomerStorageAdapter struct {
    db *PostgresDB
    }
    func (a *CustomerStorageAdapter) GetCustomer(id string) (Customer, error) {
    // 转换调用
    return a.db.FindCustomerByID(id)
    }

  4. 分层设计
    // 领域模型
    type Customer struct {
    ID string
    Name string
    Balance float64
    }
    // 仓储接口
    type CustomerRepository interface {
    Find(id string) (*Customer, error)
    Save(customer *Customer) error
    }
    // 服务层
    type CustomerService struct {
    repo CustomerRepository
    }

  5. 关键设计原则
    按需抽象
    先写具体实现
    当需要多个实现时再抽象
    保持简单

// 简单直接的接口
type Printer interface {
    Print(content string) error
}

面向行为

// 描述行为而不是状态
type PaymentProcessor interface {
    Process(payment Payment) error
}
  1. 代码整洁性的保持
    明确的职责划分
// 清晰的包结构
project/
├── domain/       // 领域模型
├── repository/   // 数据访问
├── service/      // 业务逻辑
└── api/          // 接口层

统一的错误处理

// 定义领域错误
type NotFoundError struct {
    ID string
}
func (e *NotFoundError) Error() string {
    return fmt.Sprintf("customer %s not found", e.ID)
}

良好的命名约定

type CustomerFinder interface {
    // 动词+名词的方法命名
    FindByID(id string) (*Customer, error)
}

总结:
从具体实现开始
当有多个实现需求时再抽象
保持接口小而精确
使用组合来构建复杂接口
在实际使用处定义接口
通过清晰的项目结构保持代码整洁
这样可以在不过度抽象的同时,保持代码的可读性和可维护性。

在 Go 语言中,这确实是一个值得讨论的设计问题。一般来说,在生产者端定义接口确实不是最佳实践。让我解释原因:
不好的设计(当前代码)
package store
// 在生产者包中定义大而全的接口
type CustomerStorage interface {
StoreCustomer(customer Customer) error
GetCustomer(id string) (Customer, error)
UpdateCustomer(customer Customer) error
GetAllCustomers() ([]Customer, error)
GetCustomersWithoutContract() ([]Customer, error)
GetCustomersWithNegativeBalance() ([]Customer, error)
}

更好的设计方式
在消费者端定义接口
package service
// 只定义服务需要的方法
type customerStore interface {
GetCustomer(id string) (Customer, error)
StoreCustomer(Customer) error
}
type CustomerService struct {
store customerStore
}

生产者端只提供具体实现
package store
type PostgresStore struct {
// … 具体实现细节
}
func (p *PostgresStore) GetCustomer(id string) (Customer, error) {
// 实现获取客户的逻辑
return Customer{}, nil
}
func (p *PostgresStore) StoreCustomer(c Customer) error {
// 实现存储客户的逻辑
return nil
}

为什么这样更好?
遵循接口隔离原则
消费者只需定义它们真正需要的方法
避免依赖不需要的方法
更容易维护
接口变更只影响实际使用该接口的消费者
生产者可以自由添加新方法而不影响现有消费者
更好的解耦
消费者和生产者之间的耦合更松散
更容易进行单元测试和模拟
符合 Go 的设计哲学
接口应该是小而精确的
接口应该由使用者定义
重构建议
删除生产者端的大接口
在各个消费者包中定义所需的小接口
生产者只提供具体实现
例如:

package service
type orderStore interface {
    // 只定义订单服务需要的方法
    GetCustomer(id string) (Customer, error)
}
// filepath: service/billing.go
package service
type billingStore interface {
    // 只定义账单服务需要的方法
    GetCustomersWithNegativeBalance() ([]Customer, error)
}

这种方式让代码更灵活、更易于维护,也更符合 Go 语言的设计理念。
找到具有 1 个许可证类型的类似代码

package client

import “github.com/teivah/100-go-mistakes/src/02-code-project-organization/6-interface-producer/store”

type customersGetter interface {
GetAllCustomers() ([]store.Customer, error)
}
当前的依赖结构

graph LR
client --> store[store package]
client --> customersGetter[customersGetter interface]

  1. 表面的循环依赖
    当前代码中:
package client
import "github.com/teivah/100-go-mistakes/src/02-code-project-organization/6-interface-producer/store"
type customersGetter interface {
    GetAllCustomers() ([]store.Customer, error)
}

这里看似存在循环依赖:
client 包引用了 store 包来使用 store.Customer 类型
customersGetter 接口定义在 client 包中
似乎 store 包需要实现 client 包中的接口
2. 实际上不存在循环依赖
实际上这并不是真正的循环依赖,因为:
store 包不需要导入 client 包
store 包不需要显式声明它实现了 customersGetter 接口
Go 的接口是隐式实现的
3. 更好的设计方案
package client
// 1. 在 client 包中定义自己的 Customer 类型
type Customer struct {
// 客户端需要的字段
}
type customersGetter interface {
GetAllCustomers() ([]Customer, error)
}
// 2. 使用适配器模式
type storeAdapter struct {
store store.CustomerStorage
}
func (sa *storeAdapter) GetAllCustomers() ([]Customer, error) {
storeCustomers, err := sa.store.GetAllCustomers()
if err != nil {
return nil, err
}
// 转换 store.Customer 到 client.Customer
return convertCustomers(storeCustomers), nil
}

改进建议
定义自己的类型
每个包应该定义自己需要的类型
避免直接依赖其他包的具体类型
使用适配器
在边界处转换类型
减少包之间的直接依赖
保持接口小巧
只定义真正需要的方法
避免不必要的依赖
依赖注入
使用依赖注入来管理依赖关系
提高代码的可测试性
这种方式可以让代码更加清晰和模块化,同时避免包之间的紧耦合。

client 包可以为其需求定义最精准的抽象。它与接口隔离原则(soild)中的i 概念有关。该原则指出,任何客户端都不应该被破译来他不使用的方法。因此,这种情况下,最好的方法是在生产者端公开具体的实现,并让客户端决定如何使用它以及是否需要抽象。

函数返回签名。
为什么很多情况下返回一个接口被认为是一个糟糕的做法?
如果函数返回接口而不是结构体,将会产生依赖关系。

如果考虑两个包:
client包:包含一个store 接口
store包: 包含store 的实现
在store包中,我们定义了一个实现store 接口的 inmemorystore 结构体,同时创建了一个

原则:
返回结构体而不是接口
尽可能地接收接口。

在大多数情况下,我们不应返回接口,而是应返回具体的实现。否则,由于包的依赖关系,它会使我们的设计更加复杂,并且会限制灵活性,因为所有客户端都必须依赖于相同的抽象。
同样,如果知道(而不是预见到)一个抽象将对客户端有帮助,那么可以考虑返回一个接口,否则不应该强制抽象,这些抽象应该由客户端发现。

eg. 打破规则的例子:
io包检查标准库:
LimitReader

Go 1.18 引入的泛型(Type Parameters)确实改变了某些编程模式,但并未颠覆 Rob Pike 提出的接口设计原则(如“接口应小而专注”)。以下从技术角度详细分析这一变化:


一、泛型如何减少对 interface{} 和反射的依赖

1. 替代 interface{} 的场景

泛型前(Go 1.17 及更早):
需用 interface{} 实现通用容器或算法,例如一个通用栈:

type Stack struct {
    data []interface{}
}
func (s *Stack) Push(v interface{}) { s.data = append(s.data, v) }
func (s *Stack) Pop() interface{} { /* ... */ }

问题:

  • 类型不安全(需运行时类型断言)
  • 性能损失(interface{} 涉及动态内存分配)

泛型后(Go 1.18+):
直接通过类型参数约束:

type Stack[T any] struct {
    data []T
}
func (s *Stack[T]) Push(v T) { s.data = append(s.data, v) }
func (s *Stack[T]) Pop() T { /* ... */ }

优势:

  • 编译时类型安全(无需类型断言)
  • 性能优化(避免 interface{} 的装箱拆箱)

2. 减少反射的使用

泛型前:
需用反射实现通用逻辑,例如一个合并两个 map 的函数:

func MergeMaps(a, b interface{}) interface{} {
    va := reflect.ValueOf(a)
    vb := reflect.ValueOf(b)
    // 反射检查类型、合并键值对...
}

问题:

  • 代码复杂度高
  • 运行时错误风险

泛型后:
通过类型参数直接约束 map 类型:

func MergeMaps[K comparable, V any](a, b map[K]V) map[K]V {
    merged := make(map[K]V)
    for k, v := range a { merged[k] = v }
    for k, v := range b { merged[k] = v }
    return merged
}

优势:

  • 代码简洁且类型安全
  • 无需反射即可实现通用逻辑

二、为何未颠覆接口设计原则?

1. 接口的核心作用未变

泛型解决的是数据类型的通用性问题,而接口定义的是行为的抽象。二者是互补关系:

  • 接口:定义“能做什么”(如 io.ReaderRead() 方法)。
  • 泛型:定义“处理什么类型”(如 Stack[T] 中的 T)。

示例:

// 接口定义行为(未依赖泛型)
type Processor interface {
    Process(data []byte) error
}

// 泛型定义数据类型
type Pipeline[T Processor] struct {
    processors []T
}

接口仍保持小而专注,泛型仅约束 Pipeline 处理的具体类型。


2. 小接口组合的实践仍被推崇

即使使用泛型,Go 社区仍鼓励通过小接口组合实现功能。例如:

// 小接口定义
type Cloner[T any] interface {
    Clone() T
}

// 泛型函数利用接口
func Duplicate[T Cloner[T]](original T) T {
    return original.Clone()
}

此处 Cloner 接口仅包含一个方法,符合“接口应小”的原则,泛型仅用于约束 T 必须实现该行为。


3. 泛型无法替代接口的多态性

泛型在编译时确定具体类型,而接口在运行时实现动态分发。二者适用于不同场景:

  • 泛型:适用于算法和容器(如排序、链表)。
  • 接口:适用于插件化架构或运行时多态(如不同数据库驱动实现同一接口)。

示例:

// 接口实现运行时多态
type Driver interface {
    Connect() error
}

var drivers = map[string]Driver{
    "mysql":    &MySQLDriver{},
    "postgres": &PostgresDriver{},
}

// 泛型实现编译时类型安全
func Query[T any](db *DB, sql string) ([]T, error) { /* ... */ }

三、实际场景中的协同作用

1. 标准库的实践

Go 标准库在引入泛型后,依然遵循接口设计原则:

  • slices:泛型函数(如 Sort)操作切片,但依赖元素的 Less 方法(通过接口约束)。
  • maps:泛型函数处理 map,但键仍需满足 comparable 约束(本质是内置接口)。
2. 减少反射的滥用

泛型显著减少了 JSON 解析、ORM 等场景对反射的依赖:

// 泛型替代反射解析 JSON
func ParseJSON[T any](data []byte) (T, error) {
    var result T
    if err := json.Unmarshal(data, &result); err != nil {
        return zero(T), err
    }
    return result, nil
}

四、未完全替代的场景

1. interface{} 的剩余用途

以下场景仍需 interface{}

  • 处理未知类型:如 fmt.Println 的参数。
  • 动态数据模型:如解析任意结构的 JSON(结合 map[string]interface{})。
2. 反射的必要性

反射在以下场景仍不可替代:

  • 结构体标签解析:如 json:"field"
  • 动态调用方法:如根据字符串名称调用函数。

总结

泛型的引入优化了 Go 的类型系统,减少了 interface{} 和反射的滥用,但并未改变接口设计的核心原则:

  • 泛型:解决“数据类型通用性”问题,提升类型安全和性能。
  • 接口:解决“行为抽象”问题,保持代码灵活性和可扩展性。

二者共同推动 Go 向“静态类型安全 + 动态行为抽象”的平衡发展,而非相互替代。开发者应结合场景选择工具:

  • 优先泛型:处理通用算法和容器。
  • 优先接口:定义组件交互协议。
  • 慎用反射:仅在必要时(如框架开发)使用。

http://www.kler.cn/a/549911.html

相关文章:

  • YOLOv11目标检测:解密mAP50与mAP的背后秘密
  • 代码随想录刷题攻略---动态规划---子序列问题1---子序列
  • java八股文-redis
  • python的类装饰器
  • 【系列专栏】银行IT的云原生架构-存储架构-数据库部署 10
  • 青少年编程与数学 02-009 Django 5 Web 编程 16课题、权限管理
  • rtsp rtmp 跟 http 区别
  • Kubernetes控制平面组件:etcd高可用集群搭建
  • 250214-java类集框架
  • React 前端框架搭建与解析
  • 数据结构与算法学习笔记----数位统计DP
  • (6/100)每日小游戏平台系列
  • Java爬虫获取1688商品详情API接口的设计与实现
  • 解锁机器学习核心算法 | 线性回归:机器学习的基石
  • 微服务之任务调度工具
  • 五十天精通硬件设计第32天-S参数
  • 北京青蓝智慧科技:软考高项vs考研,谁更胜一筹?
  • DeepSeek人机对话使用教程(PC版)
  • llama.cpp将sensor格式的大模型转化为gguf格式
  • 麻将对对碰游戏:规则与模拟实现