ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

GO [ 接口 ]

GO [ 接口 ] 接口前面我们已经学习了 Go 的变量、常量、数据类型、输入输出、条件控制、切片、字符串、映射表、指针、结构体、函数和方法。接下来开始学习 Go 语言中描述“对象能做什么”的核心机制接口interface。很多初学者会把接口理解成“必须由类显式实现的一组方法”。这个理解更接近 Java 或 C#但 Go 的接口有自己的设计类型不需要声明 implements只要方法集满足接口要求就自动实现接口。按照 Go 官方语言规范Interface types 的定义接口类型定义了一组方法和类型约束一个值只要属于接口的类型集合就实现这个接口。简单理解结构体关注“数据是什么”方法关注“类型能做什么”接口只关注“调用者需要什么能力”。接口把调用方和具体实现解耦。业务代码只依赖接口就可以把内存存储替换成文件存储、数据库存储或测试替身而不必重写业务逻辑。本章按照“官方定义 → 基本接口 → 隐式实现 → 接口值 → 类型断言 → 类型 switch → nil 接口 → 接口嵌入 → 泛型约束 → 实现存储数据结构”的顺序展开重点学习接口声明和隐式实现方法集与接口的关系值接收者、指针接收者对接口实现的影响接口值的动态类型和动态值类型断言与 comma-ok 写法类型 switch空接口 anynil 接口和 typed nil接口嵌入和标准库接口泛型中的类型集合使用接口实现可替换的内存存储。本文代码在 go1.27.0 darwin/arm64 环境中实际编译运行。运行结果中的地址、顺序和系统相关信息可能因环境不同而变化。接口的基本声明接口使用 interface 关键字声明最基本的接口只包含方法type Speaker interface { Speak() string }Speaker 没有规定具体数据结构只规定实现者必须提供 Speak() string 方法。定义一个类型并实现它package main import fmt type Speaker interface { Speak() string } type Dog struct { Name string } func (dog Dog) Speak() string { return dog.Name : wang } func announce(speaker Speaker) { fmt.Println(speaker.Speak()) } func main() { dog : Dog{Name: 旺财} announce(dog) }运行结果旺财: wangDog 没有写任何 implements 关键字但它拥有 Speaker 所要求的 Speak 方法因此 Dog 自动实现 Speaker。接口是一种行为约束。announce 不需要知道传入的是 Dog、Robot 还是其他类型它只需要确认参数能够 Speak。隐式实现接口Go 接口的实现关系是隐式的type Reader interface { Read() string } type File struct { Content string } func (file File) Read() string { return file.Content }File 自动实现 Reader。这个关系由编译器根据方法集判断。一个类型可以同时实现多个接口type Closer interface { Close() error } func (file File) Close() error { return nil }此时 File 同时实现 Reader 和 Closer也就可以分别传给接收这两个接口的函数。一个接口也可以被多个完全不相关的类型实现type Cat struct { Name string } func (cat Cat) Speak() string { return cat.Name : meow }Cat 和 Dog 没有共同的父类型但它们都可以实现 Speaker。接口描述的是能力不描述继承关系。编译期接口检查虽然 Go 不要求显式声明实现接口但可以使用空白标识符做编译期检查var _ Speaker Dog{}这句话不会创建有用的运行时变量它的作用是要求 Dog 可以赋值给 Speaker。如果 Dog 的 Speak 方法被删除或签名被修改编译器会立即报错。如果只有指针类型实现接口应当这样检查var _ Speaker (*Robot)(nil)这种写法特别适合公共库。接口和实现代码距离很远时编译期断言可以把实现关系固定在实现文件附近。值接收者和指针接收者接口是否被实现取决于方法集而不是“看起来有没有同名方法”。package main import fmt type Speaker interface { Speak() string } type Dog struct { Name string } func (dog Dog) Speak() string { return dog.Name : wang } type Robot struct { Name string } func (robot *Robot) Speak() string { return robot.Name : beep } func announce(speaker Speaker) { fmt.Println(speaker.Speak()) } func main() { dog : Dog{Name: 旺财} robot : Robot{Name: R2} announce(dog) announce(robot) }运行结果旺财: wang R2: beepDog 的 Speak 使用值接收者因此 Dog 和 *Dog 都实现 Speaker。Robot 的 Speak 使用指针接收者因此只有 *Robot 实现 Speaker。下面的代码不能编译var robot Robot // var speaker Speaker robot // 编译错误 var speaker Speaker robot // 正确这里有一个容易混淆的地方robot.Speak()如果 robot 是一个可寻址的普通变量Go 可以自动把调用转换为(robot).Speak()但接口赋值不是普通方法调用。把 robot 放进接口时编译器不会为了满足接口而偷偷把 Robot 变成 *Robot。接口中的动态类型必须在赋值时就满足接口。方法集速查表类型方法集T接收者为 T 的方法*T接收者为 T 和 *T 的方法interface接口声明的方法所以在设计接口实现时应当明确判断是让 T 实现接口还是让 *T 实现接口。需要修改对象状态的实现通常使用指针接收者。接口值是什么接口变量不只是“一个可以放任意值的盒子”。可以把接口值理解成一对信息接口值 动态类型 动态值 var speaker Speaker Dog{Name: 旺财} 动态类型Dog 动态值{Name: 旺财}当接口既没有动态类型也没有动态值时它才等于 nilpackage main import fmt type Speaker interface { Speak() string } func main() { var speaker Speaker fmt.Println(接口是否为 nil:, speaker nil) speaker Dog{Name: 旺财} fmt.Println(接口是否为 nil:, speaker nil) fmt.Printf(动态类型: %T\n, speaker) fmt.Println(动态值:, speaker) } type Dog struct { Name string } func (dog Dog) Speak() string { return dog.Name : wang }运行结果接口是否为 nil: true 接口是否为 nil: false 动态类型: main.Dog 动态值: {旺财}接口变量声明后没有赋值时动态类型和动态值都为空所以等于 nil。接口一旦保存了具体类型即使具体值看起来是空的接口本身也可能不再是 nil。空接口和 any没有任何方法的接口叫空接口interface{}从 Go 1.18 开始any 是 interface{} 的别名var value any value 10 value Go value []int{1, 2, 3}因为所有类型都至少拥有零个方法所以所有类型都实现空接口。空接口可以保存任意值但调用者失去了静态类型信息使用前通常需要类型断言或类型 switch。类型断言如果接口中保存了具体值可以使用类型断言取出它package main import fmt func main() { var value any 42 number : value.(int) fmt.Println(数字:, number) }运行结果数字: 42类型断言写成 value.(T)意思是“我确认 value 中保存的是 T请把它取出来”。如果断言类型不匹配程序会 panicvar value any 42 // text : value.(string) // panic更安全的写法是使用两个返回值value, ok : value.(string) if ok { fmt.Println(字符串:, value) } else { fmt.Println(不是字符串) }完整示例package main import fmt func main() { var value any 42 number, ok : value.(int) fmt.Println(int:, number, ok) text, ok : value.(string) fmt.Println(string:, text, ok) }运行结果int: 42 true string: falsecomma-ok 写法把“断言失败”转换成普通的布尔结果适合处理用户输入、配置数据和动态插件等不确定来源。断言接口类型类型断言的目标也可以是另一个接口package main import fmt type Speaker interface { Speak() string } type Named interface { Name() string } type Dog struct { Value string } func (dog Dog) Speak() string { return dog.Value : wang } func (dog Dog) Name() string { return dog.Value } func main() { var speaker Speaker Dog{Value: 旺财} named, ok : speaker.(Named) fmt.Println(实现 Named:, ok) if ok { fmt.Println(名称:, named.Name()) } }运行结果实现 Named: true 名称: 旺财speaker 的静态类型是 Speaker但它里面的动态类型 Dog 同时实现了 Named因此断言可以成功。类型 switch当需要根据动态类型执行不同逻辑时可以使用类型 switchpackage main import fmt func describe(value any) { switch current : value.(type) { case nil: fmt.Println(nil) case int: fmt.Println(整数:, current) case string: fmt.Println(字符串:, current) case []int: fmt.Println(整数切片:, current) default: fmt.Printf(其他类型: %T\n, current) } } func main() { describe(nil) describe(10) describe(Go) describe([]int{1, 2}) describe(true) }运行结果nil 整数: 10 字符串: Go 整数切片: [1 2] 其他类型: bool类型 switch 的变量 current 在每个 case 中会拥有对应分支的具体类型。相比连续写很多类型断言类型 switch 的分支结构更清晰。类型 switch 和接口方法类型 switch 也可以同时判断一个具体类型和一个接口type Stringer interface { String() string } func printValue(value any) { switch current : value.(type) { case string: fmt.Println(current) case Stringer: fmt.Println(current.String()) default: fmt.Println(无法转换成文本) } }这里先匹配 string再匹配 Stringer。具体类型和接口类型都可以出现在类型 switch 的 case 中。nil 接口和 typed nil这是 Go 接口中最容易踩坑的地方之一。一个接口只有在动态类型和动态值都为空时才是 nil。package main import fmt type ErrorReporter interface { Report() string } type ReportError struct { Message string } func (errorValue *ReportError) Report() string { if errorValue nil { return nil report error } return errorValue.Message } func main() { var pointer *ReportError var reporter ErrorReporter pointer fmt.Println(指针是否为 nil:, pointer nil) fmt.Println(接口是否为 nil:, reporter nil) fmt.Println(报告内容:, reporter.Report()) }运行结果指针是否为 nil: true 接口是否为 nil: false 报告内容: nil report errorreporter 内部保存了动态类型 *ReportError动态值是 nil 指针。因此 reporter ! nil。如果 Report 方法没有处理 nil 接收者调用 reporter.Report() 可能会 panic。错误返回值经常使用指针类型实现所以写错误判断时一定要关注“接口是否为 nil”和“接口内部的指针是否为 nil”这两个层次。下面这个判断不能检查接口内部的 typed nilif reporter nil { // 这里不会执行 }如果需要判断具体指针是否为空应该在构造接口之前检查或者在方法内部定义清晰的 nil 行为。接口嵌入接口可以嵌入其他接口把多个小能力组合成一个更大的接口type Reader interface { Read() string } type Closer interface { Close() error } type ReadCloser interface { Reader Closer }实现 ReadCloser 的类型必须同时拥有 Read 和 Closepackage main import fmt type Reader interface { Read() string } type Closer interface { Close() error } type ReadCloser interface { Reader Closer } type Document struct { Content string Closed bool } func (document *Document) Read() string { return document.Content } func (document *Document) Close() error { document.Closed true return nil } func consume(document ReadCloser) { fmt.Println(读取:, document.Read()) fmt.Println(关闭错误:, document.Close()) } func main() { document : Document{Content: Go interface} consume(document) fmt.Println(已关闭:, document.Closed) }运行结果读取: Go interface 关闭错误: nil 已关闭: true接口嵌入和结构体嵌入的思想类似都是组合小能力。但接口嵌入只组合方法要求不会提供任何实现代码。接口应该尽量小。io.Reader 只包含 Read 方法io.Writer 只包含 Write 方法调用方可以根据需要组合它们。小接口更容易被不同类型实现也更容易在测试中替换。标准库中的接口Go 标准库大量使用接口来描述行为。fmt.Stringer实现 String() string就可以被 fmt 包当作可打印文本package main import fmt type Point struct { X int Y int } func (point Point) String() string { return fmt.Sprintf((%d, %d), point.X, point.Y) } func main() { fmt.Println(Point{X: 3, Y: 4}) }运行结果(3, 4)fmt.Println 检测到 Point 实现了 fmt.Stringer就会调用 String 方法。io.Writerio.Writer 的定义非常小type Writer interface { Write(p []byte) (n int, err error) }任何实现这个方法的类型都可以被需要 io.Writer 的函数使用。下面定义一个把字节追加到切片的 Writerpackage main import ( fmt ) type ByteBuffer []byte func (buffer *ByteBuffer) Write(data []byte) (int, error) { *buffer append(*buffer, data...) return len(data), nil } func main() { var buffer ByteBuffer fmt.Fprintf(buffer, hello %s, Go) fmt.Println(string(buffer)) }运行结果hello GoByteBuffer 不需要知道 fmt.Fprintf 的内部实现只要满足 io.Writer 的方法签名就可以接收格式化输出。sort.Interfacesort.Interface 要求三个方法type Interface interface { Len() int Less(i, j int) bool Swap(i, j int) }给一个命名切片实现这三个方法就可以交给 sort.Sortpackage main import ( fmt sort ) type Scores []int func (scores Scores) Len() int { return len(scores) } func (scores Scores) Less(i, j int) bool { return scores[i] scores[j] } func (scores Scores) Swap(i, j int) { scores[i], scores[j] scores[j], scores[i] } func main() { scores : Scores{90, 70, 85} sort.Sort(scores) fmt.Println(scores) }运行结果[70 85 90]sort.Sort 不关心 Scores 是怎么保存数据的只依赖 Len、Less 和 Swap 三个行为。这就是接口把算法和数据结构分离开的例子。接口和依赖倒置接口最实用的场景之一是让高层业务依赖行为而不是依赖具体实现。不使用接口时业务函数可能直接依赖 MemoryStorefunc SaveUser(store *MemoryStore, user User) error { return store.Set(user.ID, user.Name) }这样写会把业务和内存实现绑死。如果以后换成数据库就需要修改 SaveUser 的参数和实现。使用接口后type UserStore interface { Set(id int, name string) error Get(id int) (string, bool) } func SaveUser(store UserStore, user User) error { return store.Set(user.ID, user.Name) }SaveUser 只依赖 UserStore 的两个方法。MemoryStore、DatabaseStore 和 FakeStore 都可以实现 UserStore调用者可以根据场景选择具体实现。接口应该由使用方定义而不是由实现方把所有方法都暴露出来。调用者需要什么就定义多小的接口。这样可以降低耦合也能避免接口因为实现细节不断膨胀。泛型中的接口和类型集合Go 1.18 之后接口除了可以描述方法还可以描述类型集合。这样的接口主要用于泛型约束type Number interface { int | int64 | float64 }可以使用这个约束定义泛型函数func Sum[T Number](values []T) T { var total T for _, value : range values { total value } return total }完整示例package main import fmt type Number interface { int | int64 | float64 } func Sum[T Number](values []T) T { var total T for _, value : range values { total value } return total } func main() { fmt.Println(Sum([]int{1, 2, 3})) fmt.Println(Sum([]float64{1.5, 2.5})) }运行结果6 4带类型元素的接口约束不能像普通接口一样作为变量类型使用type Number interface { int | int64 | float64 } // var value Number // 编译错误接口包含类型约束不能作为普通值类型这种接口描述的是“哪些类型可以作为类型参数”不是“运行时接口值可以调用哪些方法”。需要区分两类接口接口用途典型写法使用位置行为接口Read、Write、Close普通变量、参数、返回值类型约束int、float64、comparable泛型类型参数如果类型集合中需要包含自定义类型的底层类型可以使用 ~type Integer interface { ~int | ~int64 }这样底层类型是 int 或 int64 的自定义类型也可以满足约束。使用接口实现一个内存存储接下来实现一个完整的小数据结构基于接口的键值存储。我们先定义业务真正需要的能力type Store interface { Set(key string, value int) error Get(key string) (int, bool) Delete(key string) bool Len() int Range(func(key string, value int) bool) }Store 不关心数据保存在哪里只规定设置、读取、删除、统计数量和遍历五个操作。使用 map 实现 MemoryStoretype MemoryStore struct { data map[string]int } func NewMemoryStore() *MemoryStore { return MemoryStore{ data: make(map[string]int), } } func (store *MemoryStore) Set(key string, value int) error { if key { return fmt.Errorf(key 不能为空) } store.data[key] value return nil } func (store *MemoryStore) Get(key string) (int, bool) { value, ok : store.data[key] return value, ok } func (store *MemoryStore) Delete(key string) bool { if _, ok : store.data[key]; !ok { return false } delete(store.data, key) return true } func (store *MemoryStore) Len() int { return len(store.data) } func (store *MemoryStore) Range(visit func(key string, value int) bool) { for key, value : range store.data { if !visit(key, value) { return } } }这里 MemoryStore 用 map 保存数据指针接收者负责修改内部 map。Range 接收一个函数让调用者决定如何处理每条数据如果回调返回 false就提前结束遍历。使用 Store 接口编写业务函数func SumStore(store Store) int { total : 0 store.Range(func(key string, value int) bool { total value return true }) return total } func PrintStore(store Store) { store.Range(func(key string, value int) bool { fmt.Printf(%s%d\n, key, value) return true }) }SumStore 和 PrintStore 都不依赖 MemoryStore。以后新增 FileStore 或 DatabaseStore只要实现 Store就可以直接复用它们。完整运行代码package main import fmt type Store interface { Set(key string, value int) error Get(key string) (int, bool) Delete(key string) bool Len() int Range(func(key string, value int) bool) } type MemoryStore struct { data map[string]int } func NewMemoryStore() *MemoryStore { return MemoryStore{ data: make(map[string]int), } } func (store *MemoryStore) Set(key string, value int) error { if key { return fmt.Errorf(key 不能为空) } store.data[key] value return nil } func (store *MemoryStore) Get(key string) (int, bool) { value, ok : store.data[key] return value, ok } func (store *MemoryStore) Delete(key string) bool { if _, ok : store.data[key]; !ok { return false } delete(store.data, key) return true } func (store *MemoryStore) Len() int { return len(store.data) } func (store *MemoryStore) Range(visit func(key string, value int) bool) { for key, value : range store.data { if !visit(key, value) { return } } } func SumStore(store Store) int { total : 0 store.Range(func(key string, value int) bool { total value return true }) return total } func main() { store : NewMemoryStore() fmt.Println(写入 a:, store.Set(a, 10)) fmt.Println(写入 b:, store.Set(b, 20)) fmt.Println(空 key:, store.Set(, 99)) value, ok : store.Get(a) fmt.Println(读取 a:, value, ok) fmt.Println(数量:, store.Len()) fmt.Println(总和:, SumStore(store)) fmt.Println(删除 a:, store.Delete(a)) fmt.Println(删除 a:, store.Delete(a)) fmt.Println(删除后数量:, store.Len()) }运行结果写入 a: nil 写入 b: nil 空 key: key 不能为空 读取 a: 10 true 数量: 2 总和: 30 删除 a: true 删除 a: false 删除后数量: 1这个例子体现了接口的实际价值Store 只描述业务需要的能力MemoryStore 负责 map 的具体存储细节SumStore 只依赖 Store可以复用到其他实现方法负责封装数据结构的增删改查回调函数让 Range 的遍历行为可以由调用者决定。增加一个带日志的装饰器接口还可以用于包装已有实现。下面定义一个 LoggingStore它内部保存一个 Store并在写入时打印日志type LoggingStore struct { next Store } func (store LoggingStore) Set(key string, value int) error { fmt.Println(Set:, key, value) return store.next.Set(key, value) } func (store LoggingStore) Get(key string) (int, bool) { return store.next.Get(key) } func (store LoggingStore) Delete(key string) bool { fmt.Println(Delete:, key) return store.next.Delete(key) } func (store LoggingStore) Len() int { return store.next.Len() } func (store LoggingStore) Range(visit func(string, int) bool) { store.next.Range(visit) }使用时只需要把原来的 Store 包一层base : NewMemoryStore() var store Store LoggingStore{next: base} store.Set(a, 10)业务函数仍然接收 Store不需要知道现在是否增加了日志。这个模式在真实项目中经常用于日志、指标、缓存和权限控制。常见错误和避坑提醒误区一接口需要显式 implementsGo 不需要 implements 关键字。类型只要方法集满足接口就自动实现接口。误区二认为有同名方法就一定实现接口方法名相同还不够参数列表、返回值、接收者类型都必须符合要求。指针接收者方法还会影响 T 和 *T 哪个实现接口。误区三把 nil 接口和 typed nil 混为一谈接口中只要保存了动态类型即使动态值是 nil 指针接口本身也不等于 nil。误区四所有地方都使用 anyany 可以保存任意值但会失去静态类型检查。能使用具体类型或小接口时不要为了“通用”而把参数全部写成 any。误区五接口设计得过大一个接口包含的方法越多实现它的类型越少。优先定义调用方真正需要的小接口例如 io.Reader、io.Writer而不是一次性定义几十个方法。误区六把类型约束接口当普通接口值包含 int、string 或 ~int 等类型元素的接口主要用于泛型约束不能声明普通运行时变量。误区七在接口中暴露实现细节接口应该描述稳定的业务能力。把 map、缓存、数据库连接等内部细节直接暴露出去会让实现难以替换也会增加调用方耦合。误区八忽略接口调用带来的动态分派接口调用需要根据动态类型找到对应方法。绝大多数业务代码不需要为了这点成本提前优化但在极端热路径上应通过基准测试判断而不是凭感觉改成复杂的类型 switch。一份可以直接复制的综合示例下面的例子把接口、类型断言、方法集和错误处理放在一起模拟不同格式的消息发送器package main import fmt type Sender interface { Send(message string) error } type EmailSender struct { Address string } func (sender EmailSender) Send(message string) error { fmt.Printf(发送邮件到 %s: %s\n, sender.Address, message) return nil } type SMSender struct { Phone string } func (sender *SMSender) Send(message string) error { if sender nil { return fmt.Errorf(短信发送器为空) } fmt.Printf(发送短信到 %s: %s\n, sender.Phone, message) return nil } func notify(sender Sender, message string) error { if sender nil { return fmt.Errorf(sender 接口为空) } return sender.Send(message) } func main() { email : EmailSender{Address: roseexample.com} sms : SMSender{Phone: 13800000000} fmt.Println(邮件错误:, notify(email, Go interface)) fmt.Println(短信错误:, notify(sms, Go interface)) var empty Sender fmt.Println(空接口错误:, notify(empty, 不会发送)) }运行结果发送邮件到 roseexample.com: Go interface 邮件错误: nil 发送短信到 13800000000: Go interface 短信错误: nil 空接口错误: sender 接口为空这里 EmailSender 使用值接收者值和指针都可以实现 SenderSMSender 使用指针接收者调用者必须传入 *SMSender。notify 只依赖 Sender不需要知道具体发送渠道。总结本章我们学习了 Go 接口的完整基础接口描述的是行为不描述具体数据结构Go 接口通过方法集隐式实现不需要 implements值接收者通常让 T 和 *T 都实现接口指针接收者通常只有 *T 实现接口接口值由动态类型和动态值组成只有动态类型和动态值都为空时接口才等于 nil类型断言用于取出接口中的具体值类型 switch 可以根据动态类型选择处理逻辑any 是空接口的别名可以保存任意值但会牺牲静态类型信息小接口更容易实现也更适合解耦调用方和具体实现接口嵌入可以组合多个行为要求泛型类型集合接口用于约束类型参数不等同于普通运行时接口标准库通过 fmt.Stringer、io.Writer、sort.Interface 等接口实现可组合能力接口可以让同一套业务逻辑替换内存、文件、数据库和测试实现map、方法和接口结合后可以实现可替换的数据结构与存储层。真正理解 Go 接口之后就不会把它简单看成“方法声明的集合”。更准确的理解是接口定义调用者真正需要的能力方法集决定具体类型能否提供这些能力接口值保存动态类型和动态值而小接口让业务代码可以在不修改逻辑的情况下替换实现。官方资料Go 语言规范Interface typesGo 语言规范Method setsGo 官方教程InterfacesGo 官方文档Effective Go - InterfacesGo 官方博客JSON and GoGo 官方博客The Laws of ReflectionGo 官方文档io.Reader 和 io.WriterGo 官方文档sort.InterfaceGo 官方博客Errors are values
返回列表