GO [ 接口 ]

接口

前面我们已经学习了 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

这个例子体现了接口的实际价值:

  1. Store 只描述业务需要的能力;
  2. MemoryStore 负责 map 的具体存储细节;
  3. SumStore 只依赖 Store,可以复用到其他实现;
  4. 方法负责封装数据结构的增删改查;
  5. 回调函数让 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 接口的完整基础:

  1. 接口描述的是行为,不描述具体数据结构;
  2. Go 接口通过方法集隐式实现,不需要 implements;
  3. 值接收者通常让 T 和 *T 都实现接口;
  4. 指针接收者通常只有 *T 实现接口;
  5. 接口值由动态类型和动态值组成;
  6. 只有动态类型和动态值都为空时,接口才等于 nil;
  7. 类型断言用于取出接口中的具体值;
  8. 类型 switch 可以根据动态类型选择处理逻辑;
  9. any 是空接口的别名,可以保存任意值,但会牺牲静态类型信息;
  10. 小接口更容易实现,也更适合解耦调用方和具体实现;
  11. 接口嵌入可以组合多个行为要求;
  12. 泛型类型集合接口用于约束类型参数,不等同于普通运行时接口;
  13. 标准库通过 fmt.Stringer、io.Writer、sort.Interface 等接口实现可组合能力;
  14. 接口可以让同一套业务逻辑替换内存、文件、数据库和测试实现;
  15. map、方法和接口结合后,可以实现可替换的数据结构与存储层。

真正理解 Go 接口之后,就不会把它简单看成"方法声明的集合"。更准确的理解是:接口定义调用者真正需要的能力,方法集决定具体类型能否提供这些能力,接口值保存动态类型和动态值,而小接口让业务代码可以在不修改逻辑的情况下替换实现。

官方资料:

相关推荐
babe小鑫1 小时前
金融工程专业秋招:金融类证书与数据分析类证书的搭配方案
大数据·数据库·人工智能
夜雪一千1 小时前
Python URL解码详解:单层、双重URL解码实战与踩坑指南
linux·服务器·python
程序员_小兵1 小时前
PCIe Switch Fabric 技术解析
服务器·c语言·嵌入式硬件·php·fabric
Zs05091 小时前
服务器使用率高处理思路
服务器
咯哦哦哦哦1 小时前
配置VNC sever 6.11.0版本 linux(激活码)
linux·运维·服务器
傲世仙尊1 小时前
序列化与自定义协议-把结构体拆成字节流再拼回来
linux·服务器·网络·arm开发
User_芊芊君子2 小时前
数据库手记:从数据建模到 Spark 对接的实测记录
大数据·数据库·spark
꯭自꯭闭꯭2 小时前
DM主备集群以及读写分离集群搭建
linux·运维·数据库