刚学 Go 的时候,interface 很容易让人困惑。
在 Java、C# 这类语言里,一个类型通常要显式写:
implements SomeInterface
但 Go 不是这样。
Go 里没有 implements 关键字。一个类型只要拥有接口要求的方法,就自动实现了这个接口。
这听起来很轻,但也正是 Go 接口最重要的地方:
接口不是某个类型的父类。
接口描述的是一组行为。
本文会从新手视角详细介绍 Go 的接口,包括:
-
interface 是什么
-
如何定义接口
-
什么叫隐式实现
-
接口值内部到底装了什么
-
方法集和指针接收者的关系
-
空接口
interface{}和any -
类型断言和类型选择
-
标准库里的常见接口
-
nil 接口的坑
-
实战中应该怎么设计接口
读完以后,你应该能看懂标准库里的 io.Reader、fmt.Stringer、error,也能在自己的代码里写出更清楚的接口。
一句话理解接口
先记住这句话:
Go 的接口是一组方法签名的集合。
例如:
type Speaker interface {
Speak() string
}
这个接口的意思是:
只要某个类型有 Speak() string 方法,它就是 Speaker。
接口关心的是"你能做什么",不关心"你是谁"。
第一个接口例子
先看一段完整代码:
package main
import "fmt"
// Speaker 是一个接口。
// 它要求实现者必须有 Speak() string 方法。
type Speaker interface {
Speak() string
}
// Dog 是一个普通结构体。
type Dog struct {
Name string
}
// Dog 拥有 Speak() string 方法。
// 因此 Dog 自动实现了 Speaker 接口。
func (d Dog) Speak() string {
return d.Name + " says: woof"
}
// Introduce 接收一个 Speaker。
// 它不关心传进来的是 Dog、Cat 还是其他类型。
// 它只关心这个值能不能 Speak。
func Introduce(s Speaker) {
fmt.Println(s.Speak())
}
func main() {
dog := Dog{Name: "Lucky"}
Introduce(dog)
}
输出:
Lucky says: woof
这里最关键的一点是:
func (d Dog) Speak() string
因为 Dog 有了 Speak() string 方法,所以它自动满足 Speaker 接口。
你不需要写:
// Go 没有这种写法
type Dog implements Speaker
这就是 Go 的隐式实现。
什么是隐式实现
隐式实现的意思是:
类型不需要声明自己实现了哪个接口。
只要方法对得上,编译器就认为它实现了接口。
再加两个类型:
package main
import "fmt"
type Speaker interface {
Speak() string
}
type Dog struct {
Name string
}
func (d Dog) Speak() string {
return d.Name + " says: woof"
}
type Cat struct {
Name string
}
func (c Cat) Speak() string {
return c.Name + " says: meow"
}
type Robot struct {
ID int
}
func (r Robot) Speak() string {
return fmt.Sprintf("robot %d says: beep", r.ID)
}
func Introduce(s Speaker) {
fmt.Println(s.Speak())
}
func main() {
Introduce(Dog{Name: "Lucky"})
Introduce(Cat{Name: "Mimi"})
Introduce(Robot{ID: 1001})
}
输出:
Lucky says: woof
Mimi says: meow
robot 1001 says: beep
Dog、Cat、Robot 是完全不同的类型,但它们都有 Speak() string 方法,所以都可以当作 Speaker 使用。
这就是接口带来的好处:
调用方可以依赖行为,而不是依赖具体类型。
接口变量里装的是什么
接口变量看起来像一个普通变量:
var s Speaker
但它内部可以理解成两部分:
动态类型
动态值
例如:
package main
import "fmt"
type Speaker interface {
Speak() string
}
type Dog struct {
Name string
}
func (d Dog) Speak() string {
return d.Name + " says: woof"
}
func main() {
var s Speaker
s = Dog{Name: "Lucky"}
fmt.Printf("type=%T\n", s)
fmt.Printf("value=%v\n", s)
fmt.Println(s.Speak())
}
输出:
type=main.Dog
value={Lucky}
Lucky says: woof
此时:
接口变量 s 的动态类型是 Dog
接口变量 s 的动态值是 Dog{Name: "Lucky"}
接口变量本身的静态类型是 Speaker,但运行时里面装的是 Dog。
接口让函数更灵活
假设你一开始写了一个函数,只能处理 Dog:
func IntroduceDog(d Dog) {
fmt.Println(d.Speak())
}
这个函数的问题是:它和 Dog 绑定死了。
如果后来你有 Cat、Robot,就要继续写:
func IntroduceCat(c Cat) {
fmt.Println(c.Speak())
}
func IntroduceRobot(r Robot) {
fmt.Println(r.Speak())
}
这明显重复。
接口可以把函数改成:
func Introduce(s Speaker) {
fmt.Println(s.Speak())
}
现在任何会 Speak 的类型都能传进来。
这就是接口的核心价值:
用更小的行为抽象,替代更死的具体类型依赖。
方法签名必须完全匹配
接口要求的是方法签名。
方法签名包括:
-
方法名
-
参数列表
-
返回值列表
例如:
type Speaker interface {
Speak() string
}
下面这个类型没有实现 Speaker:
type Person struct {
Name string
}
// 返回值不匹配:这里没有返回 string。
func (p Person) Speak() {
fmt.Println("hello")
}
虽然方法也叫 Speak,但签名是:
Speak()
接口要求的是:
Speak() string
所以不匹配。
下面这个也不匹配:
// 参数不匹配:接口要求没有参数。
func (p Person) Speak(lang string) string {
return "hello"
}
Go 的接口匹配是静态检查。方法签名不完全一致,编译器不会放过。
接口可以包含多个方法
接口不只能有一个方法。
例如:
type Animal interface {
Speak() string
Move() string
}
要实现这个接口,类型必须同时拥有这两个方法:
package main
import "fmt"
type Animal interface {
Speak() string
Move() string
}
type Bird struct {
Name string
}
func (b Bird) Speak() string {
return b.Name + " says: chirp"
}
func (b Bird) Move() string {
return b.Name + " flies"
}
func Describe(a Animal) {
fmt.Println(a.Speak())
fmt.Println(a.Move())
}
func main() {
Describe(Bird{Name: "Rio"})
}
输出:
Rio says: chirp
Rio flies
如果 Bird 少了 Move(),就不能作为 Animal 使用。
小接口更常见
虽然接口可以包含多个方法,但 Go 里更推荐小接口。
标准库里有很多非常小的接口:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type Stringer interface {
String() string
}
这些接口都很小,但非常有用。
小接口的好处是:
-
更容易实现
-
更容易测试
-
更容易复用
-
更不容易过度设计
如果一个接口里有十几个方法,新手应该先警惕:是不是抽象得太早了?
方法集:值接收者和指针接收者
接口最容易卡人的地方之一,是方法集。
先看值接收者:
package main
import "fmt"
type Speaker interface {
Speak() string
}
type Dog struct {
Name string
}
// 值接收者。
func (d Dog) Speak() string {
return d.Name + " says: woof"
}
func main() {
var s1 Speaker = Dog{Name: "Lucky"}
var s2 Speaker = &Dog{Name: "Mimi"}
fmt.Println(s1.Speak())
fmt.Println(s2.Speak())
}
这段代码能编译。
因为 Dog 的方法是值接收者:
func (d Dog) Speak() string
所以:
Dog 实现了 Speaker
*Dog 也实现了 Speaker
再看指针接收者:
package main
import "fmt"
type Incrementer interface {
Inc()
}
type Counter struct {
N int
}
// 指针接收者。
// 这个方法需要修改 Counter,所以用 *Counter。
func (c *Counter) Inc() {
c.N++
}
func main() {
counter := Counter{}
var inc Incrementer = &counter
inc.Inc()
fmt.Println(counter.N)
}
这段代码能编译。
但下面这样不行:
// 编译错误:Counter 没有实现 Incrementer。
// var inc Incrementer = counter
原因是:
如果方法接收者是 *Counter,那么实现接口的是 *Counter,不是 Counter。
新手可以先记住这个规则:
值接收者:T 和 *T 通常都能满足接口。
指针接收者:通常只有 *T 能满足接口。
为什么 c.Inc() 可以,但接口赋值不行
你可能会问:
counter := Counter{}
counter.Inc()
这不是能调用吗?
是的。因为 counter 是一个可寻址变量,Go 可以帮你自动取地址,等价于:
(&counter).Inc()
但接口赋值更严格:
var inc Incrementer = counter
这里编译器检查的是 Counter 的方法集是否包含 Inc()。由于 Inc() 属于 *Counter 的方法集,不属于 Counter 的方法集,所以赋值失败。
这不是 Go 故意刁难,而是为了让接口实现规则保持清楚。
编译期检查接口实现
有时你希望明确告诉读者:
这个类型必须实现某个接口。
可以写编译期检查:
package main
type Speaker interface {
Speak() string
}
type Dog struct{}
func (d Dog) Speak() string {
return "woof"
}
// 这行不会生成实际运行逻辑。
// 它只是在编译期检查 Dog 是否实现了 Speaker。
var _ Speaker = Dog{}
func main() {}
如果 Dog 没有实现 Speaker,这行就会编译失败。
如果方法是指针接收者,常见写法是:
var _ SomeInterface = (*SomeType)(nil)
例如:
type Closer interface {
Close() error
}
type File struct{}
func (f *File) Close() error {
return nil
}
var _ Closer = (*File)(nil)
这是一种很常见的 Go 代码习惯。
空接口 interface{} 和 any
空接口写作:
interface{}
它没有任何方法要求。
因为没有要求,所以所有类型都满足它。
例如:
package main
import "fmt"
func PrintAny(v interface{}) {
fmt.Printf("type=%T value=%v\n", v, v)
}
func main() {
PrintAny(123)
PrintAny("hello")
PrintAny(true)
PrintAny([]int{1, 2, 3})
}
输出:
type=int value=123
type=string value=hello
type=bool value=true
type=[]int value=[1 2 3]
Go 1.18 引入了 any,它是 interface{} 的别名。
所以下面两个写法等价:
func PrintAny(v interface{}) {}
func PrintAny(v any) {}
现在更推荐使用 any 表达"任意类型"。
但要注意:
any 不是没有类型。
any 表示这里可以接收任意类型的值。
如果你想对里面的具体类型做不同处理,仍然需要类型断言或类型选择。
类型断言:从接口里取出具体类型
接口变量里装着动态类型和动态值。
如果你想把接口值还原成某个具体类型,可以用类型断言。
package main
import "fmt"
func main() {
var v any = "hello"
s, ok := v.(string)
if !ok {
fmt.Println("v is not a string")
return
}
fmt.Println("string value:", s)
}
输出:
string value: hello
类型断言的常见写法是:
value, ok := x.(SomeType)
如果 x 里面确实装着 SomeType,ok 为 true。
如果不是,ok 为 false。
不推荐新手直接省略 ok
你也可以这样写:
s := v.(string)
但如果 v 里面不是 string,程序会 panic。
所以新手更推荐写:
s, ok := v.(string)
这更安全。
类型选择 type switch
如果你要判断多种类型,可以用 type switch。
package main
import "fmt"
func Describe(v any) {
switch value := v.(type) {
case int:
fmt.Println("int:", value)
case string:
fmt.Println("string:", value)
case bool:
fmt.Println("bool:", value)
default:
fmt.Printf("unknown type %T: %v\n", value, value)
}
}
func main() {
Describe(100)
Describe("go")
Describe(true)
Describe(3.14)
}
输出:
int: 100
string: go
bool: true
unknown type float64: 3.14
type switch 很适合处理 any、解析数据、做通用日志等场景。
但不要滥用。
如果你发现自己到处都在 type switch,可能说明接口设计不够好。很多时候应该让类型自己实现方法,而不是在外面不断判断类型。
标准库里的接口:error
Go 里最常见的接口之一是 error。
它的定义可以理解为:
type error interface {
Error() string
}
只要一个类型有 Error() string 方法,它就可以当作错误返回。
示例:
package main
import (
"fmt"
)
type ConfigError struct {
Field string
}
func (e ConfigError) Error() string {
return "missing config field: " + e.Field
}
func LoadConfig() error {
return ConfigError{Field: "database_url"}
}
func main() {
err := LoadConfig()
if err != nil {
fmt.Println(err)
}
}
输出:
missing config field: database_url
error 之所以强大,是因为函数可以只声明返回 error:
func LoadConfig() error
调用方不需要知道具体错误类型,先按错误处理即可。
如果调用方确实关心具体错误类型,再用类型断言或 errors.As。
标准库里的接口:fmt.Stringer
fmt.Stringer 是另一个常见接口。
它的定义是:
type Stringer interface {
String() string
}
如果一个类型实现了 String() string,fmt 打印它时会使用这个方法。
package main
import "fmt"
type User struct {
ID int
Name string
}
func (u User) String() string {
return fmt.Sprintf("User<ID=%d, Name=%s>", u.ID, u.Name)
}
func main() {
user := User{ID: 1, Name: "Alice"}
fmt.Println(user)
}
输出:
User<ID=1, Name=Alice>
这就是接口的漂亮之处:
fmt.Println 不需要专门认识 User。
它只要发现 User 实现了 String() string,就知道怎么打印。
标准库里的接口:io.Reader
io.Reader 是 Go 里非常经典的接口。
它的定义是:
type Reader interface {
Read(p []byte) (n int, err error)
}
意思是:
任何能把数据读进 []byte 的类型,都可以叫 Reader。
比如文件、网络连接、字符串读取器、压缩流都可以实现 io.Reader。
来看一个字符串读取例子:
package main
import (
"fmt"
"io"
"strings"
)
func main() {
reader := strings.NewReader("hello go")
buffer := make([]byte, 4)
for {
n, err := reader.Read(buffer)
if n > 0 {
fmt.Printf("read %d bytes: %q\n", n, buffer[:n])
}
if err == io.EOF {
break
}
if err != nil {
fmt.Println("read error:", err)
break
}
}
}
可能输出:
read 4 bytes: "hell"
read 4 bytes: "o go"
为什么 io.Reader 这么重要?
因为很多函数只需要"能读数据的东西",并不关心数据来自哪里。
例如你可以写:
func CountBytes(r io.Reader) (int, error) {
total := 0
buffer := make([]byte, 1024)
for {
n, err := r.Read(buffer)
total += n
if err == io.EOF {
return total, nil
}
if err != nil {
return total, err
}
}
}
这个函数可以统计任何 io.Reader 的字节数。
它可以接收:
-
文件
-
字符串
-
网络连接
-
bytes.Buffer
-
压缩解码器
-
HTTP 响应体
这就是接口带来的扩展性。
标准库里的接口:io.Writer
io.Writer 的定义是:
type Writer interface {
Write(p []byte) (n int, err error)
}
意思是:
任何能写入 []byte 的类型,都可以叫 Writer。
示例:
package main
import (
"bytes"
"fmt"
)
func main() {
var buffer bytes.Buffer
fmt.Fprintln(&buffer, "hello")
fmt.Fprintln(&buffer, "go")
fmt.Print(buffer.String())
}
输出:
hello
go
fmt.Fprintln 接收的是 io.Writer,所以它既能写到文件,也能写到 bytes.Buffer,还能写到网络连接。
函数只依赖一个很小的接口,就能适配很多具体类型。
接口组合
接口可以组合其他接口。
例如标准库里常见的组合思想:
type ReadWriter interface {
Reader
Writer
}
你也可以写自己的组合接口:
package main
import "fmt"
type Reader interface {
Read() string
}
type Writer interface {
Write(value string)
}
type ReadWriter interface {
Reader
Writer
}
type Memory struct {
data string
}
func (m *Memory) Read() string {
return m.data
}
func (m *Memory) Write(value string) {
m.data = value
}
func SaveAndPrint(rw ReadWriter, value string) {
rw.Write(value)
fmt.Println(rw.Read())
}
func main() {
mem := &Memory{}
SaveAndPrint(mem, "hello interface")
}
输出:
hello interface
组合接口适合表达"同时需要多种能力"的场景。
但仍然要注意:不要为了抽象而抽象。只有确实需要组合能力时再组合。
nil 接口的坑
接口里的 nil 是 Go 新手常踩的坑。
先看普通 nil 接口:
package main
import "fmt"
type Speaker interface {
Speak() string
}
func main() {
var s Speaker
fmt.Println(s == nil)
}
输出:
true
因为此时接口变量里没有动态类型,也没有动态值。
再看一个容易误解的例子:
package main
import "fmt"
type Notifier interface {
Notify()
}
type Email struct {
Address string
}
func (e *Email) Notify() {
if e == nil {
fmt.Println("empty email")
return
}
fmt.Println("send email to", e.Address)
}
func main() {
var email *Email = nil
var n Notifier = email
fmt.Println(n == nil)
n.Notify()
}
输出:
false
empty email
为什么 n == nil 是 false?
因为此时接口变量 n 里有动态类型:
*Email
只是它的动态值是 nil。
可以理解成:
n = (动态类型: *Email, 动态值: nil)
接口本身不是空的,所以 n != nil。
这就是接口 nil 的核心规则:
只有动态类型和动态值都为空时,接口才等于 nil。
实际项目里,如果函数返回 error 或其他接口,要小心不要返回"装着 nil 指针的接口"。
接口和测试
接口很适合让代码更容易测试。
假设你有一个用户服务,它需要从存储层读取用户名。
你可以先定义服务真正需要的行为:
package main
import (
"fmt"
)
type UserStore interface {
FindName(id int) (string, error)
}
type UserService struct {
store UserStore
}
func NewUserService(store UserStore) *UserService {
return &UserService{store: store}
}
func (s *UserService) Greeting(id int) (string, error) {
name, err := s.store.FindName(id)
if err != nil {
return "", err
}
return "hello, " + name, nil
}
type MemoryUserStore struct {
users map[int]string
}
func (m MemoryUserStore) FindName(id int) (string, error) {
name, ok := m.users[id]
if !ok {
return "", fmt.Errorf("user %d not found", id)
}
return name, nil
}
func main() {
store := MemoryUserStore{
users: map[int]string{
1: "Alice",
},
}
service := NewUserService(store)
msg, err := service.Greeting(1)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println(msg)
}
输出:
hello, Alice
UserService 不关心数据来自内存、数据库还是远程 API。
它只依赖一个小接口:
type UserStore interface {
FindName(id int) (string, error)
}
测试时,你可以写一个假的实现:
type FakeUserStore struct{}
func (FakeUserStore) FindName(id int) (string, error) {
return "TestUser", nil
}
这样测试 UserService 时就不需要真的连数据库。
接口应该定义在哪里
这是 Go 接口设计里很重要的一点。
在很多语言里,接口常常由实现方定义。
例如一个数据库包可能会先定义:
type Database interface {
FindUser(id int) (User, error)
SaveUser(user User) error
DeleteUser(id int) error
ListUsers() ([]User, error)
BeginTransaction() error
Commit() error
Rollback() error
}
然后业务层被迫依赖这个大接口。
Go 更常见的做法是:
由使用方定义它需要的最小接口。
如果业务层只需要查用户名,那就定义:
type UserNameFinder interface {
FindName(id int) (string, error)
}
这样实现方只要提供这一个方法,就能被业务层使用。
这也是 Go 里常说的:
Accept interfaces, return concrete types.
可以理解成:
函数参数可以接收接口,让调用方更灵活。
函数返回值优先返回具体类型,让调用方拿到更明确的能力。
当然这不是绝对规则,但对新手很有帮助。
不要过早设计接口
新手很容易一上来就写接口:
type UserService interface {
CreateUser(name string) error
DeleteUser(id int) error
UpdateUser(id int, name string) error
FindUser(id int) (User, error)
ListUsers() ([]User, error)
}
然后再写一个实现:
type userService struct{}
如果项目里只有一个实现,而且暂时没有测试替身、没有扩展需求,这种接口可能只是增加复杂度。
更稳的做法是:
-
先写具体类型。
-
当调用方真的需要替换实现时,再抽出接口。
-
接口只包含调用方真正需要的方法。
接口不是越多越好。
好的接口应该让代码更简单,而不是让代码更绕。
接口和泛型的区别
Go 1.18 之后,接口还有一个新用途:作为泛型约束。
例如:
package main
import "fmt"
type Number interface {
~int | ~int64 | ~float64
}
func Add[T Number](a, b T) T {
return a + b
}
func main() {
fmt.Println(Add(1, 2))
fmt.Println(Add(1.5, 2.5))
}
这里的 Number 也是接口,但它不是普通的"方法集合接口",而是类型约束。
它表示:
T 的底层类型可以是 int、int64 或 float64。
新手可以先这样区分:
普通接口:用于运行时多态,描述对象能做什么。
泛型约束接口:用于编译期约束,描述类型参数允许是什么。
如果你刚开始学 Go,不需要急着深入泛型接口。先把普通接口、方法集、类型断言、nil 陷阱掌握好,更重要。
常见错误总结
错误一:把接口当继承
Go 接口不是父类。
不要把接口理解成:
Dog 继承 Speaker
更准确的理解是:
Dog 拥有 Speak() string 方法,所以 Dog 满足 Speaker。
错误二:接口太大
接口越大,实现它越困难。
如果一个函数只需要读取数据,就接收 io.Reader,不要接收一个拥有十几个方法的大对象。
错误三:返回不必要的接口
如果构造函数只有一个明确实现,通常返回具体类型:
func NewMemoryUserStore() *MemoryUserStore {
return &MemoryUserStore{}
}
不要为了"抽象"而写:
func NewMemoryUserStore() UserStore {
return &MemoryUserStore{}
}
返回具体类型更灵活。调用方如果只需要接口,可以自己把它赋给接口变量。
错误四:到处使用 any
any 很方便,但它会丢失具体类型信息。
如果你的函数明明只接收字符串,就写:
func PrintName(name string)
不要写:
func PrintName(name any)
除非你真的要处理任意类型。
错误五:忽略 nil 接口
接口值是否为 nil,取决于动态类型和动态值是否都为空。
看到这种代码要小心:
var p *MyError = nil
var err error = p
fmt.Println(err == nil) // false
如果你想返回没有错误,应该直接返回 nil:
return nil
不要返回一个 nil 指针包装成的 error。
一个完整小练习:支付接口
最后用一个小练习把接口串起来。
需求:
订单系统不关心具体支付方式。
它只关心支付对象能不能 Pay。
代码:
package main
import "fmt"
type Payer interface {
Pay(amount int) error
}
type OrderService struct {
payer Payer
}
func NewOrderService(payer Payer) *OrderService {
return &OrderService{payer: payer}
}
func (s *OrderService) Checkout(orderID string, amount int) error {
fmt.Printf("checkout order %s\n", orderID)
if err := s.payer.Pay(amount); err != nil {
return err
}
fmt.Println("checkout success")
return nil
}
type WeChatPay struct{}
func (WeChatPay) Pay(amount int) error {
fmt.Printf("wechat pay: %d cents\n", amount)
return nil
}
type AliPay struct{}
func (AliPay) Pay(amount int) error {
fmt.Printf("alipay: %d cents\n", amount)
return nil
}
func main() {
wechatOrder := NewOrderService(WeChatPay{})
_ = wechatOrder.Checkout("order-001", 9900)
fmt.Println()
aliOrder := NewOrderService(AliPay{})
_ = aliOrder.Checkout("order-002", 12800)
}
输出:
checkout order order-001
wechat pay: 9900 cents
checkout success
checkout order order-002
alipay: 12800 cents
checkout success
这里的关键是:
type Payer interface {
Pay(amount int) error
}
OrderService 不依赖 WeChatPay,也不依赖 AliPay。
它只依赖 Payer 这个行为。
以后你要加银行卡支付:
type BankCardPay struct{}
func (BankCardPay) Pay(amount int) error {
fmt.Printf("bank card pay: %d cents\n", amount)
return nil
}
OrderService 不需要改。
这就是接口带来的扩展性。
学习路线建议
如果你是新手,可以按这个顺序练习接口:
-
定义只有一个方法的小接口。
-
写两个结构体,让它们都实现这个接口。
-
写一个函数接收接口参数。
-
练习值接收者和指针接收者的区别。
-
用
fmt.Stringer给结构体自定义打印格式。 -
用
io.Reader写一个能接收多种输入源的函数。 -
用类型断言和 type switch 处理
any。 -
故意写一次 nil 接口例子,理解为什么
err != nil。 -
在测试里用小接口替代真实依赖。
这几步练熟以后,接口就不会再显得抽象。
总结
Go 接口的核心可以压缩成几句话:
-
接口是一组方法签名。
-
类型只要拥有接口要求的方法,就自动实现接口。
-
Go 没有
implements关键字。 -
接口变量内部包含动态类型和动态值。
-
值接收者和指针接收者会影响类型是否满足接口。
-
interface{}和any表示任意类型。 -
类型断言可以从接口中取回具体类型。
-
小接口比大接口更常见,也更容易维护。
-
接口应该在使用方按需求定义。
-
不要为了抽象而抽象。
最后记住一句:
Go 的接口不是为了制造层级,而是为了描述行为。
当你能用"这个函数真正需要什么行为"来思考接口时,你就开始真正掌握 Go 的接口了。