引入
Go 接口 学了怎么定义和实现接口,但接口设计得好不好是另一回事。Go 社区和标准库积累了大量接口设计经验,核心原则就是”接口要小”。这篇整理实际开发中最常用的接口设计模式和标准库经典接口。
正文
定义
定义
接口最佳实践是 Go 社区总结的接口设计原则:接口要小而专注,一两个方法就够;通过接口组合拼出大接口;尽量使用标准库已有的接口(
io.Reader、io.Writer、fmt.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.Reader | Read(p []byte) (int, error) | 可读数据源 |
io.Writer | Write(p []byte) (int, error) | 可写目标 |
io.Closer | Close() error | 可关闭资源 |
io.ReadWriter | Reader + Writer | 可读可写 |
io.ReadWriteCloser | Reader + Writer + Closer | 可读可写可关闭 |
fmt.Stringer | String() string | 自定义字符串表示 |
error | Error() string | 错误信息 |
sort.Interface | Len, Less, Swap | 可排序集合 |
常见写法
| 写法 | 说明 |
|---|---|
| 一两个方法的接口 | 小接口,容易实现和组合 |
| 接口嵌入接口 | 组合出更大的接口 |
| 参数用接口类型 | 调用者可以替换实现 |
| 返回具体类型 | 保持返回值明确 |
实现 String() | 自定义 fmt.Println 输出 |
实现 Error() | 自定义错误类型 |
| 在使用处定义接口 | 只包含需要的方法 |
特点
- Go 推崇小接口,一两个方法就够
- 通过接口组合拼出复杂行为,不用一开始就设计大接口
- 标准库接口(
io.Reader、io.Writer、fmt.Stringer、error)尽量复用 - 接受接口,返回结构体是 Go 社区的通用原则
- 在使用处定义接口,不要在实现处提前抽象
- 不要为接口而接口,先写具体类型,需要多态时再提取接口
- 实现标准库接口让代码和生态自动互通
理解
Go 接口设计的核心就一句话:接口要小。一个方法就是一个接口,需要更多能力就组合起来。标准库的 io.Reader、io.Writer、fmt.Stringer 都是这么设计的,简单又通用。记住”接受接口,返回结构体”,在需要的地方才定义接口,代码就会既灵活又不过度设计。
引出
接口和多态搞定了,接下来进入 Go 最有特色的部分——并发。看 Go 并发基础,学习怎么用 goroutine 和 channel 写出并发程序。