刚学 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: beepDog、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()) }输出typemain.Dog value{Lucky} Lucky says: woof此时接口变量 s 的动态类型是 Dog 接口变量 s 的动态值是 Dog{Name: Lucky}接口变量本身的静态类型是Speaker但运行时里面装的是Dog。接口让函数更灵活假设你一开始写了一个函数只能处理Dogfunc 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 }下面这个类型没有实现Speakertype 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}) }输出typeint value123 typestring valuehello typebool valuetrue 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里面确实装着SomeTypeok为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.14type switch 很适合处理any、解析数据、做通用日志等场景。但不要滥用。如果你发现自己到处都在 type switch可能说明接口设计不够好。很多时候应该让类型自己实现方法而不是在外面不断判断类型。标准库里的接口errorGo 里最常见的接口之一是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_urlerror之所以强大是因为函数可以只声明返回errorfunc LoadConfig() error调用方不需要知道具体错误类型先按错误处理即可。如果调用方确实关心具体错误类型再用类型断言或errors.As。标准库里的接口fmt.Stringerfmt.Stringer是另一个常见接口。它的定义是type Stringer interface { String() string }如果一个类型实现了String() stringfmt打印它时会使用这个方法。package main import fmt type User struct { ID int Name string } func (u User) String() string { return fmt.Sprintf(UserID%d, Name%s, u.ID, u.Name) } func main() { user : User{ID: 1, Name: Alice} fmt.Println(user) }输出UserID1, NameAlice这就是接口的漂亮之处fmt.Println 不需要专门认识 User。 它只要发现 User 实现了 String() string就知道怎么打印。标准库里的接口io.Readerio.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.Writerio.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 gofmt.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, AliceUserService不关心数据来自内存、数据库还是远程 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{} }返回具体类型更灵活。调用方如果只需要接口可以自己把它赋给接口变量。错误四到处使用 anyany很方便但它会丢失具体类型信息。如果你的函数明明只接收字符串就写func PrintName(name string)不要写func PrintName(name any)除非你真的要处理任意类型。错误五忽略 nil 接口接口值是否为 nil取决于动态类型和动态值是否都为空。看到这种代码要小心var p *MyError nil var err error p fmt.Println(err nil) // false如果你想返回没有错误应该直接返回 nilreturn 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 的接口了。参考资料A Tour of Go: InterfacesA Tour of Go: Interface valuesA Tour of Go: Type assertionsThe Go Programming Language Specification: Interface typesEffective Go: Interfaces and methodsPackage io: Reader and WriterPackage fmt: StringerBuiltin package: any and errorGo Wiki: CodeReviewComments - Interfaces