引入

Go 接口 学了怎么定义和实现接口,但接口设计得好不好是另一回事。Go 社区和标准库积累了大量接口设计经验,核心原则就是”接口要小”。这篇整理实际开发中最常用的接口设计模式和标准库经典接口。

正文

定义

定义

接口最佳实践是 Go 社区总结的接口设计原则:接口要小而专注,一两个方法就够;通过接口组合拼出大接口;尽量使用标准库已有的接口(io.Readerio.Writerfmt.Stringer 等),让代码和生态互通。

语法

// 小接口:一个方法
type Reader interface {
    Read(p []byte) (n int, err error)
}
 
// 接口组合
type ReadWriter interface {
    Reader
    Writer
}

例子

小接口原则

// 好:接口小而专注
type Writer interface {
    Write(p []byte) (n int, err error)
}
 
// 不好:接口太大,方法太多
type FileHandler interface {
    Read(p []byte) (n int, err error)
    Write(p []byte) (n int, err error)
    Close() error
    Stat() FileInfo
}

小接口容易实现,容易组合。大接口让实现者被迫写一堆可能用不到的方法。

io.Reader

import "io"
 
func readAll(r io.Reader) string {
    buf := make([]byte, 1024)
    n, _ := r.Read(buf)
    return string(buf[:n])
}

io.Reader 只有一个 Read 方法。文件、网络连接、字符串、缓冲区都实现了它,函数参数用 io.Reader 就能接收任何可读的数据源

io.Writer

import "io"
 
func writeHello(w io.Writer) {
    w.Write([]byte("hello"))
}
 
func main() {
    writeHello(os.Stdout) // 写到终端
 
    f, _ := os.Create("out.txt")
    defer f.Close()
    writeHello(f) // 写到文件
}

io.Writer 只有一个 Write 方法。函数参数用 io.Writer 就能写到任何可写的目标

fmt.Stringer

import "fmt"
 
type Point struct {
    X, Y int
}
 
func (p Point) String() string {
    return fmt.Sprintf("(%d, %d)", p.X, p.Y)
}
 
func main() {
    p := Point{3, 4}
    fmt.Println(p)        // (3, 4)
    fmt.Sprintf("%v", p)  // (3, 4)
}

实现 String() string 方法就实现了 fmt.Stringer 接口,fmt.Println%v 会自动调用它来自定义输出格式。

error 接口

type MyError struct {
    Code int
    Msg  string
}
 
func (e *MyError) Error() string {
    return fmt.Sprintf("[%d] %s", e.Code, e.Msg)
}
 
func doWork() error {
    return &MyError{Code: 500, Msg: "internal error"}
}
 
func main() {
    if err := doWork(); err != nil {
        fmt.Println(err) // [500] internal error
    }
}

error 接口只有一个 Error() string 方法,实现它就变成了 Go 标准的错误类型。

接口组合

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 ReadWriteCloser interface {
    Reader
    Writer
    Closer
}

标准库的 io.ReadWriteCloser 就是这样组合出来的。小接口各自实现,需要时再组合。

接受接口,返回结构体

type Notifier interface {
    Notify(msg string)
}
 
type EmailNotifier struct{}
 
func (e EmailNotifier) Notify(msg string) {
    fmt.Println("Email:", msg)
}
 
// 函数接受接口
func Send(n Notifier, msg string) {
    n.Notify(msg)
}
 
// 返回具体类型
func NewEmailNotifier() *EmailNotifier {
    return &EmailNotifier{}
}
 
func main() {
    n := NewEmailNotifier()
    Send(n, "hello") // Email: hello
}

Go 社区的共识:函数参数用接口,返回值用具体类型。这样调用者可以灵活替换实现,返回值保持明确。

在调用处定义接口

// 在使用的地方定义,不在实现的地方
type UserStore interface {
    GetUser(id string) (*User, error)
}
 
func HandleRequest(store UserStore) {
    u, err := store.GetUser("123")
    // ...
}

Go 推荐在使用接口的地方定义接口,而不是在实现方定义。这样接口只包含当前场景真正需要的方法。

标准库常用接口速查

接口方法说明
io.ReaderRead(p []byte) (int, error)可读数据源
io.WriterWrite(p []byte) (int, error)可写目标
io.CloserClose() error可关闭资源
io.ReadWriterReader + Writer可读可写
io.ReadWriteCloserReader + Writer + Closer可读可写可关闭
fmt.StringerString() string自定义字符串表示
errorError() string错误信息
sort.InterfaceLen, Less, Swap可排序集合

常见写法

写法说明
一两个方法的接口小接口,容易实现和组合
接口嵌入接口组合出更大的接口
参数用接口类型调用者可以替换实现
返回具体类型保持返回值明确
实现 String()自定义 fmt.Println 输出
实现 Error()自定义错误类型
在使用处定义接口只包含需要的方法

特点

  • Go 推崇小接口,一两个方法就够
  • 通过接口组合拼出复杂行为,不用一开始就设计大接口
  • 标准库接口(io.Readerio.Writerfmt.Stringererror)尽量复用
  • 接受接口,返回结构体是 Go 社区的通用原则
  • 使用处定义接口,不要在实现处提前抽象
  • 不要为接口而接口,先写具体类型,需要多态时再提取接口
  • 实现标准库接口让代码和生态自动互通

理解

Go 接口设计的核心就一句话:接口要小。一个方法就是一个接口,需要更多能力就组合起来。标准库的 io.Readerio.Writerfmt.Stringer 都是这么设计的,简单又通用。记住”接受接口,返回结构体”,在需要的地方才定义接口,代码就会既灵活又不过度设计。

引出

接口和多态搞定了,接下来进入 Go 最有特色的部分——并发。看 Go 并发基础,学习怎么用 goroutine 和 channel 写出并发程序。