Skip to content

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
}

HTTPCheckerTCPChecker 都实现了 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"形式和映射读取一样:oktrue 表示断言成功,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 接口可以有 RoundRobinRandomWeighted 多种实现,网关核心逻辑只依赖接口,不依赖具体实现。

六、常见错误

接口实现不完整

类型只实现了接口的部分方法时,编译器会在赋值时报错:

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 是指针接收者方法,只有 *FileWrite。这是方法集规则的直接体现。

接口嵌套

接口可以嵌套其他接口,组合出更大的接口:

go
type Reader interface{ Read([]byte) (int, error) }
type Writer interface{ Write([]byte) (int, error) }
type ReadWriter interface {
	Reader
	Writer
}

实现 ReadWriter 的类型需要同时实现 ReadWrite。接口嵌套在 io 包中大量使用,io.ReadWriteCloser 就是嵌套了 ReaderWriterCloser