Golang 接口¶
什么是接口¶
Go 的接口是一组方法签名的集合,定义对象能做什么,不关心对象是什么类型。当某一类型满足接口定义的所有方法时,即认为该类型实现了该接口。
接口的使用¶
定义接口与使用接口¶
type 接口名 interface {
方法名1(参数列表1) (返回值列表1)
方法名2(参数列表2) (返回值列表2)
}
一个接口包含以下要素:
-
接口名
-
方法名
-
参数列表
-
返回值列表
下面是一个 Go 接口的简单示例:
Example
// ========== 阶段 1:定义接口(契约)==========
type Writer interface {
Write([]byte) (int, error) // 只是签名,没有实现
}
// ========== 阶段 2:具体类型实现(满足契约)==========
type File struct {
filename string
}
// 这里才是真正的实现
func (f File) Write(data []byte) (int, error) {
// 实现逻辑已经写好了
fmt.Printf("Writing to %s: %s\n", f.filename, string(data))
return len(data), nil
}
// ========== 阶段 3:使用接口(调用已实现的方法)==========
func main() {
file := File{filename: "test.txt"}
var w Writer = file // File 已经实现了 Write,所以可以赋值
// 这里调用的是 File 已经实现好的 Write 方法
w.Write([]byte("hello")) // 输出: Writing to test.txt: hello
}
简单来说,三个阶段分别对应三个关键点:
-
接口定义 -> 契约(方法签名集合)
-
类型实现 -> 满足契约(在类型上实现方法)
-
使用接口 -> 通过契约调用(调用已实现的方法)
空接口¶
没有任何方法声明的接口被称为空接口interface{}。
所有的类型都实现了空接口,因此interface{}(Go 1.18+ 可用 any)可以用于存储任意类型的数值,常用于需要处理未知类型的场景:
// 可以存储任何类型的值
var anything interface{} = 42
anything = "hello"
anything = []int{1, 2, 3}
Golang很多标准库函数都使用了空接口,如fmt.Println、fmt.Printf、fmt.Sprintf等,表示接受任意类型的参数。
接口作为函数参数¶
接口作为函数参数,在定义函数时,形参为接口类型;函数调用时,实参为该接口的具体实现。
package main
import "fmt"
type Reader interface {
Read() int
}
type MyReader struct {
a, b int
}
// Reader 接口实现
func (m *MyReader) Read() int {
return m.a + m.b
}
// 参数类型为接口类型
func DoJob(r Reader) {
fmt.Printf("myReader is %d\n", r.Read())
}
func main() {
myReader := &MyReader{2, 5}
DoJob(myReader) // 输出: myReader is 7
}
若形参为空接口,则实参可以是任意类型:
package main
import "fmt"
func DoJob(value interface{}) {
fmt.Printf("value is %v\n", value)
}
func main() {
val := 10
DoJob(val) // 输出: value is 10
DoJob("hello") // 输出: value is hello
DoJob([]int{1, 2, 3}) // 输出: value is [1 2 3]
}
设计理念¶
Go接口的设计理念
-
"满足即实现":只要方法匹配,自动满足接口
-
"小而专一":接口小,通过组合扩展
-
"解耦优先":类型不需要知道接口,接口不需要知道类型
-
"组合优于继承":通过组合和接口实现代码复用
这样的设计能让代码更灵活、更易测试、更易扩展。
类比虚函数¶
Go 接口类似C++中的虚函数:
// C++ 抽象基类(纯虚函数)
class Writer {
public:
virtual int Write(const std::vector<uint8_t>& data) = 0; // 纯虚函数
virtual ~Writer() = default;
};
// 具体实现类
class FileWriter : public Writer {
public:
int Write(const std::vector<uint8_t>& data) override {
// 实现逻辑
return data.size();
}
};
// 使用
std::unique_ptr<Writer> writer = std::make_unique<FileWriter>();
writer->Write({1, 2, 3});
但是 C++ 的虚函数在实现时需要显式继承(class FileWriter : public Writer);而 Go 的接口是隐式实现的:只要一个类型实现了接口中定义的所有方法,它就自动实现了该接口,无需显式声明。
与传统OOP的差异¶
以 Java 接口 为代表的传统 OOP 与 Go 接口的核心差异:
Abstract
| 特性 | Java 接口 | Go 接口 |
|---|---|---|
| 实现方式 | 显式 implements | 隐式满足(鸭子类型) |
| 类型关系 | 类层次 + 接口 | 扁平结构,无继承 |
| 接口大小 | 可大可小 | 小接口 + 组合 |
| 接口成员 | 方法 + 常量 + default/static | 仅方法签名 |
| 空类型容器 | Object 根类 | interface{} / any |
| 函数式 | 函数式接口 + Lambda | func 一等公民 |
| 耦合度 | 类型与接口编译期绑定 | 类型与接口解耦 |
| 第三方扩展 | 通常需包装或改源码 | 直接满足接口 |
-
隐式实现 vs 显式继承
在传统oop中,实现一个接口需要显式继承该接口。而在Go中,实现一个接口只需要实现该接口中定义的所有方法,无需显式继承该接口:
// Java - 必须显式声明实现接口 interface Writer { void write(byte[] data); } // 必须用 implements 关键字 class FileWriter implements Writer { @Override public void write(byte[] data) { // 实现 } }// Go - 隐式实现,无需声明 type Writer interface { Write([]byte) (int, error) } // 只要实现了方法,就自动满足接口 type FileWriter struct {} func (f FileWriter) Write(data []byte) (int, error) { // 实现 } // 无需显式声明 "implements Writer"-
传统 OOP: 编译时检查继承关系,类型与接口耦合
-
Go: 运行时检查方法集,类型与接口解耦
鸭子类型(Duck Typing)
// Java - 必须显式实现 class MyClass implements Writer { // 即使有 write 方法,不声明 implements 就不能用 }// Go - "如果它走起来像鸭子,叫起来像鸭子,那它就是鸭子" type MyType struct {} func (m MyType) Write(data []byte) (int, error) { return len(data), nil } // 自动满足 Writer 接口,无需声明 var w Writer = MyType{} // ✅ 可以 -
-
组合优于继承
在传统oop中,继承是建立类型层次的主要方式。而得以于Go接口灵活的特性,组合通常是更好的选择:
// Java - 类层次结构 class Animal { void eat() { ... } } class Dog extends Animal { void bark() { ... } } class Cat extends Animal { void meow() { ... } }// Go - 通过组合和接口 type Eater interface { Eat() } type Dog struct { // 组合其他类型 Animal Animal } func (d Dog) Eat() { ... } func (d Dog) Bark() { ... }-
传统 OOP: 静态继承关系,类型层次固定,难以灵活组合
-
Go:通过接口和组合,支持更灵活地组合行为,从而实现多态
多态与接口
多态对象与接口的概念在cs61a中有详尽的介绍。在学习完cs61a后,我曾尝试基于该思想实现了一个简易的立体几何计算器,里面记录有当时对多态对象与接口的理解。
类型系统的差异
在传统oop中,类型系统基于类层次:
// Java - 类层次 Object └── Animal ├── Dog └── Cat而在Go中,类型关系通过接口动态建立,类型系统更加扁平:
// Go - 扁平的类型系统 // 没有基类,没有继承 // 只有接口和实现 type Dog struct {} type Cat struct {} // 通过接口定义行为 type Animal interface { Eat() Sleep() } -
-
小接口原则(接口隔离)
个人认为这是一个十分显著且意义非凡的差异。
传统oop在定义接口时,通常会倾向于定义大接口,即在方法集合中尽可能包含更多的方法。这导致在实现接口时不得不统一实现所有方法,即便某些方法对于某些类型来说没必要或并不适用:
// Java - 大接口 interface FileOperations { void read(); void write(); void delete(); void rename(); void copy(); void move(); // ... 很多方法 } // 实现类必须实现所有方法 class File implements FileOperations { // 即使只需要 write,也要实现所有方法 }而在Go中,接口的定义能够做到小而专一,在使用时按需组合即可:
// Go - 小接口,按需组合 type Reader interface { Read([]byte) (int, error) } type Writer interface { Write([]byte) (int, error) } type Closer interface { Close() error } // 需要多个功能时组合接口 type ReadWriter interface { Reader Writer } type ReadWriteCloser interface { Reader Writer Closer }灵活解耦
传统oop的“大接口”决定了接口与实现者之间必然存在较高的耦合度:
// Java - 接口和实现类需要提前规划 // 如果第三方库的类型想实现你的接口,需要修改代码 interface MyInterface { void doSomething(); } // 第三方库的类型无法实现你的接口(除非修改源码)而Go接口的灵活性则允许第三方库的类型动态实现接口,而无需修改源码:
// Go - 接口定义者不需要知道实现者 // 任何类型都可以实现你的接口 type MyInterface interface { DoSomething() } // 第三方库的类型也可以实现你的接口 import "thirdparty" type ThirdPartyType struct {} func (t ThirdPartyType) DoSomething() { ... } var m MyInterface = ThirdPartyType{} // ✅ 可以
一个实际应用场景¶
// 需要定义基类或接口,所有数据源必须继承
interface DataSource {
String read();
void write(String data);
}
class FileSource implements DataSource { ... }
class DatabaseSource implements DataSource { ... }
class APISource implements DataSource { ... }
// 定义小接口
type Reader interface {
Read() (string, error)
}
type Writer interface {
Write(string) error
}
// 任何有 Read/Write 方法的类型都可以使用
// 包括标准库、第三方库、你自己的类型
func processData(r Reader, w Writer) {
data, _ := r.Read()
w.Write(data)
}
// 标准库的 os.File 自动满足 Reader/Writer
// 第三方库的类型也可以
// 你的自定义类型也可以