
接口
前面我们已经学习了 Go 的变量、常量、数据类型、输入输出、条件控制、切片、字符串、映射表、指针、结构体、函数和方法。接下来开始学习 Go 语言中描述"对象能做什么"的核心机制:接口(interface)。
很多初学者会把接口理解成"必须由类显式实现的一组方法"。这个理解更接近 Java 或 C#,但 Go 的接口有自己的设计:类型不需要声明 implements,只要方法集满足接口要求,就自动实现接口。
按照 Go 官方语言规范:Interface types 的定义,接口类型定义了一组方法和类型约束;一个值只要属于接口的类型集合,就实现这个接口。
简单理解:
- 结构体关注"数据是什么";
- 方法关注"类型能做什么";
- 接口只关注"调用者需要什么能力"。
接口把调用方和具体实现解耦。业务代码只依赖接口,就可以把内存存储替换成文件存储、数据库存储或测试替身,而不必重写业务逻辑。
本章按照"官方定义 → 基本接口 → 隐式实现 → 接口值 → 类型断言 → 类型 switch → nil 接口 → 接口嵌入 → 泛型约束 → 实现存储数据结构"的顺序展开,重点学习:
- 接口声明和隐式实现;
- 方法集与接口的关系;
- 值接收者、指针接收者对接口实现的影响;
- 接口值的动态类型和动态值;
- 类型断言与 comma-ok 写法;
- 类型 switch;
- 空接口 any;
- nil 接口和 typed nil;
- 接口嵌入和标准库接口;
- 泛型中的类型集合;
- 使用接口实现可替换的内存存储。
本文代码在 go1.27.0 darwin/arm64 环境中实际编译运行。运行结果中的地址、顺序和系统相关信息可能因环境不同而变化。
接口的基本声明
接口使用 interface 关键字声明,最基本的接口只包含方法:
Go
type Speaker interface {
Speak() string
}
Speaker 没有规定具体数据结构,只规定实现者必须提供 Speak() string 方法。
定义一个类型并实现它:
Go
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)
}
运行结果:
Go
旺财: wang
Dog 没有写任何 implements 关键字,但它拥有 Speaker 所要求的 Speak 方法,因此 Dog 自动实现 Speaker。
接口是一种行为约束。announce 不需要知道传入的是 Dog、Robot 还是其他类型,它只需要确认参数能够 Speak。
隐式实现接口
Go 接口的实现关系是隐式的:
Go
type Reader interface {
Read() string
}
type File struct {
Content string
}
func (file File) Read() string {
return file.Content
}
File 自动实现 Reader。这个关系由编译器根据方法集判断。
一个类型可以同时实现多个接口:
Go
type Closer interface {
Close() error
}
func (file File) Close() error {
return nil
}
此时 File 同时实现 Reader 和 Closer,也就可以分别传给接收这两个接口的函数。
一个接口也可以被多个完全不相关的类型实现:
Go
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: beep
Dog 的 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: "旺财"}
当接口既没有动态类型,也没有动态值时,它才等于 nil:
package 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,请把它取出来"。
如果断言类型不匹配,程序会 panic:
var 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: false
comma-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
当需要根据动态类型执行不同逻辑时,可以使用类型 switch:
package 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 error>
reporter 内部保存了动态类型 *ReportError,动态值是 nil 指针。因此 reporter != nil。
如果 Report 方法没有处理 nil 接收者,调用 reporter.Report() 可能会 panic。错误返回值经常使用指针类型实现,所以写错误判断时一定要关注"接口是否为 nil"和"接口内部的指针是否为 nil"这两个层次。
下面这个判断不能检查接口内部的 typed nil:
if reporter == nil {
// 这里不会执行
}
如果需要判断具体指针是否为空,应该在构造接口之前检查,或者在方法内部定义清晰的 nil 行为。
接口嵌入
接口可以嵌入其他接口,把多个小能力组合成一个更大的接口:
type Reader interface {
Read() string
}
type Closer interface {
Close() error
}
type ReadCloser interface {
Reader
Closer
}
实现 ReadCloser 的类型必须同时拥有 Read 和 Close:
package 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.Writer
io.Writer 的定义非常小:
type Writer interface {
Write(p []byte) (n int, err error)
}
任何实现这个方法的类型,都可以被需要 io.Writer 的函数使用。下面定义一个把字节追加到切片的 Writer:
package 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 Go
ByteBuffer 不需要知道 fmt.Fprintf 的内部实现,只要满足 io.Writer 的方法签名,就可以接收格式化输出。
sort.Interface
sort.Interface 要求三个方法:
type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}
给一个命名切片实现这三个方法,就可以交给 sort.Sort:
package 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 三个行为。这就是接口把算法和数据结构分离开的例子。
接口和依赖倒置
接口最实用的场景之一,是让高层业务依赖行为,而不是依赖具体实现。
不使用接口时,业务函数可能直接依赖 MemoryStore:
func 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 实现 MemoryStore
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
}
}
}
这里 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,不需要知道现在是否增加了日志。这个模式在真实项目中经常用于日志、指标、缓存和权限控制。
常见错误和避坑提醒
误区一:接口需要显式 implements
Go 不需要 implements 关键字。类型只要方法集满足接口,就自动实现接口。
误区二:认为有同名方法就一定实现接口
方法名相同还不够,参数列表、返回值、接收者类型都必须符合要求。指针接收者方法还会影响 T 和 *T 哪个实现接口。
误区三:把 nil 接口和 typed nil 混为一谈
接口中只要保存了动态类型,即使动态值是 nil 指针,接口本身也不等于 nil。
误区四:所有地方都使用 any
any 可以保存任意值,但会失去静态类型检查。能使用具体类型或小接口时,不要为了"通用"而把参数全部写成 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: "rose@example.com"}
sms := &SMSender{Phone: "13800000000"}
fmt.Println("邮件错误:", notify(email, "Go interface"))
fmt.Println("短信错误:", notify(sms, "Go interface"))
var empty Sender
fmt.Println("空接口错误:", notify(empty, "不会发送"))
}
运行结果:
发送邮件到 rose@example.com: Go interface
邮件错误: <nil>
发送短信到 13800000000: Go interface
短信错误: <nil>
空接口错误: sender 接口为空
这里 EmailSender 使用值接收者,值和指针都可以实现 Sender;SMSender 使用指针接收者,调用者必须传入 *SMSender。notify 只依赖 Sender,不需要知道具体发送渠道。
总结
本章我们学习了 Go 接口的完整基础:
- 接口描述的是行为,不描述具体数据结构;
- Go 接口通过方法集隐式实现,不需要 implements;
- 值接收者通常让 T 和 *T 都实现接口;
- 指针接收者通常只有 *T 实现接口;
- 接口值由动态类型和动态值组成;
- 只有动态类型和动态值都为空时,接口才等于 nil;
- 类型断言用于取出接口中的具体值;
- 类型 switch 可以根据动态类型选择处理逻辑;
- any 是空接口的别名,可以保存任意值,但会牺牲静态类型信息;
- 小接口更容易实现,也更适合解耦调用方和具体实现;
- 接口嵌入可以组合多个行为要求;
- 泛型类型集合接口用于约束类型参数,不等同于普通运行时接口;
- 标准库通过 fmt.Stringer、io.Writer、sort.Interface 等接口实现可组合能力;
- 接口可以让同一套业务逻辑替换内存、文件、数据库和测试实现;
- map、方法和接口结合后,可以实现可替换的数据结构与存储层。
真正理解 Go 接口之后,就不会把它简单看成"方法声明的集合"。更准确的理解是:接口定义调用者真正需要的能力,方法集决定具体类型能否提供这些能力,接口值保存动态类型和动态值,而小接口让业务代码可以在不修改逻辑的情况下替换实现。
官方资料: