Appearance
13|接口
接口定义一组方法签名。任何类型只要实现了这些方法,就自动满足接口,不需要显式声明"实现了哪个接口"。这种隐式实现让代码扩展更灵活——新增一个类型时,不需要修改接口定义,也不需要像 Java 那样写 implements。但灵活性的代价是:查找一个类型实现了哪些接口不太直观,需要借助 IDE 或文档。
一、接口定义与实现
接口定义一组方法:
go
package main
import "fmt"
type Checker interface {
Check() error
}
type HTTPChecker struct {
URL string
}
func (h HTTPChecker) Check() error {
// 发 HTTP 请求,检查状态码
return nil
}
type TCPChecker struct {
Host string
Port int
}
func (t TCPChecker) Check() error {
// 检查 TCP 端口是否可连
return nil
}HTTPChecker 和 TCPChecker 都实现了 Check() error,因此它们都可以赋值给 Checker 类型的变量:
go
func runCheck(c Checker) error {
return c.Check()
}
func main() {
http := HTTPChecker{URL: "https://example.com"}
tcp := TCPChecker{Host: "10.0.0.1", Port: 3306}
runCheck(http)
runCheck(tcp)
}接口的隐式实现意味着:新增一个类型时,不需要修改接口定义。这种松耦合让代码扩展更灵活,但也意味着查找一个类型实现了哪些接口不太直观,需要借助 IDE 或文档。
二、空接口
没有任何方法的接口叫空接口(interface{}),所有类型都满足空接口。它相当于"任意类型",在需要存放不同类型值的场景里使用:
go
func printAny(v interface{}) {
fmt.Printf("value=%v type=%T\n", v, v)
}
func main() {
printAny("hello")
printAny(42)
printAny(true)
}Go 1.18 以后引入了泛型,很多原本用空接口的场景可以用泛型替代,获得编译期类型检查。但空接口在反射、JSON 解析等场景仍然有用。
使用空接口里的值时,需要做类型断言:
go
func process(v interface{}) {
s, ok := v.(string)
if ok {
fmt.Println("string:", s)
return
}
i, ok := v.(int)
if ok {
fmt.Println("int:", i)
return
}
fmt.Println("unknown type")
}类型断言的"逗号 ok"形式和映射读取一样:ok 为 true 表示断言成功,false 表示类型不匹配。不用 ok 直接断言时,类型不匹配会 panic:
go
s := v.(string) // 如果 v 不是 string,panic三、类型开关
对空接口的值做多次类型判断时,用 switch 配合 .(type) 更简洁:
go
func describe(v interface{}) {
switch val := v.(type) {
case string:
fmt.Println("string length:", len(val))
case int:
fmt.Println("int value:", val)
case []string:
fmt.Println("string slice:", len(val))
default:
fmt.Printf("unknown: %T\n", val)
}
}switch 的每个 case 里,val 的类型已经被推断为对应类型,不需要再断言。
四、接口的 nil 陷阱
接口值由两部分组成:类型和值。一个接口变量为 nil,只有当类型和值都为 nil 时才成立:
go
var p *Host = nil
var c Checker = p // 接口的类型是 *Host,值是 nil
fmt.Println(c == nil) // false这个行为极其反直觉:c 里存的是一个 nil 指针,但 c 本身不是 nil。把 c 传给其他函数后,函数内部判断 if c != nil 会进入分支,然后调用方法时 panic。排查这类问题时,不能只看值是否为 nil,要理解接口的内部结构。
避免这个陷阱的方法是:返回接口的函数里,如果底层值可能为 nil,在返回前显式判断:
go
func getChecker() Checker {
var p *HTTPChecker = nil
if p == nil {
return nil // 返回真正的 nil 接口
}
return p
}五、接口抽象时机
接口不是越多越好。一个类型只有一个实现时,直接用结构体更清楚。接口适合这些场景:
| 场景 | 例子 |
|---|---|
| 有多个实现 | HTTP 检查、TCP 检查、命令检查都实现 Checker |
| 需要替换外部依赖 | 真实数据库 Client 和测试 Fake Client |
| 调用方只关心行为 | 只关心 Check(),不关心具体类型 |
go-gateway 这类项目里,路由匹配器、负载均衡器、限流器都适合用接口抽象。比如 LoadBalancer 接口可以有 RoundRobin、Random、Weighted 多种实现,网关核心逻辑只依赖接口,不依赖具体实现。
六、常见错误
接口实现不完整
类型只实现了接口的部分方法时,编译器会在赋值时报错:
go
type PartialChecker struct{}
func (p PartialChecker) Check() error { return nil }
var c Checker = PartialChecker{} // 编译错误,如果 Checker 还有其他方法排查时对比接口定义和类型方法列表,确认每个方法的签名完全一致,包括接收者类型(值 vs 指针)。
值类型不满足指针接收者方法的接口
go
type Writer interface{ Write([]byte) (int, error) }
type File struct{}
func (f *File) Write(p []byte) (int, error) { return len(p), nil }
var w Writer = File{} // 编译错误:File 没有 Write 方法Write 是指针接收者方法,只有 *File 有 Write。这是方法集规则的直接体现。
接口嵌套
接口可以嵌套其他接口,组合出更大的接口:
go
type Reader interface{ Read([]byte) (int, error) }
type Writer interface{ Write([]byte) (int, error) }
type ReadWriter interface {
Reader
Writer
}实现 ReadWriter 的类型需要同时实现 Read 和 Write。接口嵌套在 io 包中大量使用,io.ReadWriteCloser 就是嵌套了 Reader、Writer 和 Closer。