导航菜单

接口基础

接口(Interface)

接口是 Go 语言中定义行为契约的抽象类型。它声明了一组方法签名,任何实现了这些方法的类型都自动满足该接口,无需显式声明。接口是 Go 实现多态的核心机制。

接口的定义与语法

基本语法

接口使用 type 关键字和 interface 关键字定义:

// 定义接口
type Speaker interface {
    Speak() string
}

type Mover interface {
    Move(x, y float64)
}

// 多方法接口
type Animal interface {
    Speak() string
    Move(x, y float64)
    Name() string
}

接口 vs 具体类型

// 具体类型:描述数据"是什么"
type Dog struct {
    Name string
    Age  int
}

// 接口类型:描述数据"能做什么"
type Speaker interface {
    Speak() string
}

隐式实现(Duck Typing)

Duck Typing(鸭子类型)

Duck Typing 来源于一句名言:“如果它走起来像鸭子,叫起来像鸭子,那它就是鸭子。“在 Go 中,一个类型只要实现了接口要求的所有方法,就自动满足该接口,不需要显式声明 implements 关系。

隐式实现示例

package main

import "fmt"

// 定义接口
type Speaker interface {
    Speak() string
}

// 定义类型 —— 注意:这里没有 implements Speaker
type Dog struct {
    Name string
}

// Dog 实现了 Speak() 方法,自动满足 Speaker 接口
func (d Dog) Speak() string {
    return fmt.Sprintf("%s: 汪汪!", d.Name)
}

type Cat struct {
    Name string
}

// Cat 也实现了 Speak() 方法,自动满足 Speaker 接口
func (c Cat) Speak() string {
    return fmt.Sprintf("%s: 喵喵~", c.Name)
}

type Robot struct {
    Model string
}

// Robot 也实现了 Speak() 方法
func (r Robot) Speak() string {
    return fmt.Sprintf("机器人 %s: 你好!", r.Model)
}

// 接受接口类型参数的函数
func MakeSpeak(s Speaker) {
    fmt.Println(s.Speak())
}

func main() {
    dog := Dog{Name: "旺财"}
    cat := Cat{Name: "小花"}
    robot := Robot{Model: "A1"}

    MakeSpeak(dog)    // 旺财: 汪汪!
    MakeSpeak(cat)    // 小花: 喵喵~
    MakeSpeak(robot)  // 机器人 A1: 你好!
}

编译期接口检查

Go 编译器在以下场景会检查类型是否实现了接口:

// 场景 1:赋值给接口变量
var s Speaker = Dog{}  // 编译器检查 Dog 是否实现了 Speaker

// 场景 2:函数参数
func Greet(s Speaker) {}
Greet(Dog{})  // 编译器检查

// 场景 3:显式编译期检查(推荐在包初始化时使用)
var _ Speaker = (*Dog)(nil)  // 确保 Dog 实现了 Speaker,否则编译报错
var _ Speaker = (*Cat)(nil)  // 同时检查指针接收者

值接收者 vs 指针接收者与接口

type Greeter interface {
    Greet() string
}

type Person struct {
    Name string
}

// 值接收者实现
func (p Person) Greet() string {
    return "你好, " + p.Name
}

func main() {
    // 值接收者:值和指针都可以赋给接口
    var g Greeter
    g = Person{"Alice"}     // ✅ 值可以直接赋给接口
    g = &Person{"Bob"}      // ✅ 指针也可以赋给接口
    fmt.Println(g.Greet())
}
type Writer interface {
    Write(data []byte) error
}

type File struct {
    content []byte
}

// 指针接收者实现
func (f *File) Write(data []byte) error {
    f.content = append(f.content, data...)
    return nil
}

func main() {
    var w Writer
    // w = File{}       // ❌ 编译错误:File 没有实现 Writer(值接收者没有 Write 方法)
    w = &File{}        // ✅ 指针接收者实现了接口
}

接口值的内部结构

接口值(Interface Value)

接口值在 Go 运行时内部由两个部分组成:类型(type)值(value),也称为 (T, V) 对。只有当两者都为 nil 时,接口值才等于 nil。

内部结构图解

┌──────────────────────────┐
│      接口值 (iface)       │
├─────────────┬────────────┤
│   类型       │    值      │
│  (dynamic    │ (dynamic   │
│   type)      │  value)    │
│             │            │
│  *Dog       │  {Name:    │
│             │   "旺财"}  │
└─────────────┴────────────┘

示例:观察接口值的类型和值

package main

import "fmt"

type Speaker interface {
    Speak() string
}

type Dog struct{ Name string }
func (d Dog) Speak() string { return d.Name + ": 汪汪!" }

func describe(i Speaker) {
    fmt.Printf("类型: %T, 值: %v\n", i, i)
}

func main() {
    var s Speaker          // 零值:<nil>
    describe(s)            // 类型: <nil>, 值: <nil>

    s = Dog{"旺财"}
    describe(s)            // 类型: main.Dog, 值: {旺财}
    fmt.Println(s.Speak()) // 旺财: 汪汪!
}

接口值与 nil 的微妙关系

这是 Go 中最容易踩坑的知识点之一:

package main

import "fmt"

type MyError struct {
    Message string
}

func (e *MyError) Error() string {
    return e.Message
}

func returnsError() error {
    var p *MyError = nil  // p 是 *MyError 类型的 nil 指针
    return p              // 返回的是 (type=*MyError, value=nil)
    // 注意:这不是 nil error!
}

func main() {
    err := returnsError()
    fmt.Println(err == nil)  // false!

    // 原因:err 的内部结构是 (*MyError, nil)
    // 类型部分不是 nil,所以整个接口值不是 nil
}

空接口 interface / any

空接口(Empty Interface)

空接口 interface{} 是一个没有任何方法要求的接口。由于任何类型都至少实现了零个方法,因此所有类型都自动满足空接口。从 Go 1.18 起,anyinterface{} 的类型别名,推荐使用 any

基本用法

package main

import "fmt"

func printAny(a any) {  // any 等价于 interface{}
    fmt.Printf("值: %v, 类型: %T\n", a, a)
}

func main() {
    printAny(42)              // 值: 42, 类型: int
    printAny("hello")         // 值: hello, 类型: string
    printAny(3.14)            // 值: 3.14, 类型: float64
    printAny(true)            // 值: true, 类型: bool
    printAny([]int{1, 2, 3})  // 值: [1 2 3], 类型: []int
    printAny(map[string]int{"a": 1})  // 值: map[a:1], 类型: map[string]int
}

空接口的典型使用场景

// 场景 1:fmt 包的参数
func Println(a ...any)  // fmt.Println 接受任意数量、任意类型的参数

// 场景 2:JSON 解析(第三方结构未知)
var data any
json.Unmarshal([]byte(`{"name":"Alice","age":25}`), &data)
// data 的类型是 map[string]any

// 场景 3:容器存储混合类型(不推荐,有更好的替代方案)
items := []any{1, "hello", 3.14, true}

// 场景 4:函数参数或返回值类型不确定
func DoSomething(config any) error { ... }

空接口 vs 泛型(Go 1.18+)

从 Go 1.18 起,许多之前需要空接口的场景可以使用泛型替代:

// 之前:使用空接口 + 类型断言
func First(items []any) any {
    if len(items) > 0 {
        return items[0]
    }
    return nil
}

// 之后:使用泛型(类型安全)
func First[T any](items []T) T {
    var zero T
    if len(items) > 0 {
        return items[0]
    }
    return zero
}

// 调用时类型安全
nums := []int{1, 2, 3}
first := First(nums)  // first 的类型是 int,无需类型断言

常用标准接口

Stringer 接口

fmt.Stringer 是最常用的标准接口之一,定义在 fmt 包中:

// fmt.Stringer 接口定义
type Stringer interface {
    String() string
}

当类型实现了 Stringerfmt.Printlnfmt.Printf 等函数会自动调用 String() 方法:

package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

// 实现 Stringer 接口
func (p Person) String() string {
    return fmt.Sprintf("%s (年龄: %d)", p.Name, p.Age)
}

func main() {
    alice := Person{Name: "Alice", Age: 30}
    fmt.Println(alice)  // Alice (年龄: 30) — 自动调用 String()
    fmt.Printf("%s\n", alice)  // Alice (年龄: 30)

    // 对比:没有实现 Stringer 的情况
    type PlainPerson struct {
        Name string
        Age  int
    }
    bob := PlainPerson{Name: "Bob", Age: 25}
    fmt.Println(bob)  // {Bob 25} — 默认格式化
}

error 接口

error 是 Go 中最基础的错误处理接口,定义在 builtin 包中:

// error 接口定义
type error interface {
    Error() string
}
package main

import (
    "errors"
    "fmt"
)

// 自定义错误类型
type NotFoundError struct {
    Resource string
    ID       int
}

// 实现 error 接口
func (e *NotFoundError) Error() string {
    return fmt.Sprintf("%s (ID: %d) 未找到", e.Resource, e.ID)
}

func findUser(id int) (*string, error) {
    if id <= 0 {
        return nil, &NotFoundError{Resource: "用户", ID: id}
    }
    name := "Alice"
    return &name, nil
}

func main() {
    user, err := findUser(-1)
    if err != nil {
        fmt.Println(err)  // 用户 (ID: -1) 未找到

        // 类型断言获取更详细的错误信息
        if notFound, ok := err.(*NotFoundError); ok {
            fmt.Printf("资源: %s, ID: %d\n", notFound.Resource, notFound.ID)
        }
        return
    }
    fmt.Println(*user)
}

io.Reader 和 io.Writer

io.Readerio.Writer 是 Go 中数据读写的基础抽象,也是小接口原则的经典代表:

// io.Reader:从数据源读取字节
type Reader interface {
    Read(p []byte) (n int, err error)
}

// io.Writer:将字节写入目标
type Writer interface {
    Write(p []byte) (n int, err error)
}
package main

import (
    "fmt"
    "io"
    "os"
    "strings"
)

// 通用的数据处理函数:读取所有输入并转换为大写后输出
func ProcessAndPrint(r io.Reader, w io.Writer) error {
    data, err := io.ReadAll(r)
    if err != nil {
        return err
    }

    // 转换为大写
    for i, b := range data {
        if b >= 'a' && b <= 'z' {
            data[i] = b - 32
        }
    }

    _, err = w.Write(data)
    return err
}

func main() {
    // 从字符串读取
    r := strings.NewReader("hello, world!")

    // 输出到标准输出
    ProcessAndPrint(r, os.Stdout)
    // 输出: HELLO, WORLD!

    // 输出到缓冲区
    var buf strings.Builder
    ProcessAndPrint(strings.NewReader("go programming"), &buf)
    fmt.Println(buf.String())  // GO PROGRAMMING
}

其他常用标准接口

接口方法说明
io.ReaderioRead(p []byte) (n int, err error)数据读取
io.WriterioWrite(p []byte) (n int, err error)数据写入
io.CloserioClose() error关闭资源
io.ReadCloserioReader + Closer可读取且可关闭
fmt.StringerfmtString() string自定义字符串表示
errorbuiltinError() string错误表示
sort.InterfacesortLen() int, Less(i, j int) bool, Swap(i, j int)排序接口
json.Marshalerencoding/jsonMarshalJSON() ([]byte, error)自定义 JSON 序列化

练习题

练习 1:实现几何图形接口

定义 Shape 接口,要求包含 Area() float64Perimeter() float64 方法。然后分别为 CircleRectangle 实现该接口,并编写函数计算所有图形的总面积。

参考答案

解题思路:先定义接口和结构体,然后为每个结构体实现接口方法,最后编写接受接口切片的函数。

代码

package main

import (
    "fmt"
    "math"
)

// 定义接口
type Shape interface {
    Area() float64
    Perimeter() float64
}

// 圆形
type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.Radius
}

func (c Circle) String() string {
    return fmt.Sprintf("圆形(半径=%.2f)", c.Radius)
}

// 矩形
type Rectangle struct {
    Width, Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

func (r Rectangle) String() string {
    return fmt.Sprintf("矩形(宽=%.2f, 高=%.2f)", r.Width, r.Height)
}

// 计算所有图形的总面积
func TotalArea(shapes []Shape) float64 {
    total := 0.0
    for _, s := range shapes {
        total += s.Area()
    }
    return total
}

func main() {
    shapes := []Shape{
        Circle{Radius: 5},
        Rectangle{Width: 3, Height: 4},
        Circle{Radius: 2},
    }

    for _, s := range shapes {
        fmt.Printf("%s — 面积: %.2f, 周长: %.2f\n", s, s.Area(), s.Perimeter())
    }

    fmt.Printf("\n总面积: %.2f\n", TotalArea(shapes))
    // 总面积: 98.69(圆面积78.54 + 矩形面积12.00 + 圆面积12.57)
}

关键点[]Shape 切片可以存储任何实现了 Shape 接口的类型值,体现了接口的多态性。

练习 2:自定义错误类型与接口

编写函数 Divide(a, b float64) (float64, error),当 b 为 0 时返回自定义错误 DivisionByZeroError。实现 error 接口和 fmt.Stringer 接口,确保错误信息清晰可读。

参考答案

解题思路:定义自定义错误类型,分别实现 errorfmt.Stringer 接口。注意 fmt 包在格式化时会优先使用 Stringer 接口。

代码

package main

import "fmt"

// 自定义错误类型
type DivisionByZeroError struct {
    Dividend float64
}

// 实现 error 接口
func (e DivisionByZeroError) Error() string {
    return fmt.Sprintf("除零错误: %.2f / 0", e.Dividend)
}

// 实现 fmt.Stringer 接口
func (e DivisionByZeroError) String() string {
    return fmt.Sprintf("DivisionByZeroError{被除数=%.2f}", e.Dividend)
}

func Divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, DivisionByZeroError{Dividend: a}
    }
    return a / b, nil
}

func main() {
    // 正常情况
    result, err := Divide(10, 3)
    if err != nil {
        fmt.Println("错误:", err)
    } else {
        fmt.Printf("10 / 3 = %.4f\n", result)
        // 10 / 3 = 3.3333
    }

    // 错误情况
    result, err = Divide(10, 0)
    if err != nil {
        fmt.Println("错误:", err)
        // 错误: 除零错误: 10.00 / 0

        // 类型断言获取具体错误类型
        if divErr, ok := err.(DivisionByZeroError); ok {
            fmt.Println("详细信息:", divErr)
            // 详细信息: DivisionByZeroError{被除数=10.00}
        }
    } else {
        fmt.Printf("结果: %.4f\n", result)
    }
}

关键点fmt 包在输出时会优先调用 String() 方法(如果实现了 Stringer),而不是 Error()。注意 errorStringer 是两个独立的接口,需要分别实现。

练习 3:接口值的 nil 陷阱

以下代码的输出是什么?请解释原因。

package main

import "fmt"

type MyError struct {
    Msg string
}

func (e *MyError) Error() string {
    return e.Msg
}

func getError(shouldFail bool) error {
    if !shouldFail {
        var err *MyError = nil
        return err
    }
    return &MyError{Msg: "出错了"}
}

func main() {
    err := getError(false)
    fmt.Printf("err == nil: %v\n", err == nil)
    fmt.Printf("err 的值: %v, 类型: %T\n", err, err)
}
参考答案

输出

err == nil: false
err 的值: <nil>, 类型: *main.MyError

分析

  1. getError(false) 中,var err *MyError = nil 创建了一个类型为 *MyError 的 nil 指针
  2. return err 将这个 nil 指针赋给 error 接口类型的返回值
  3. 此时接口值的内部结构是 (type=*MyError, value=nil)
  4. 接口值等于 nil 的条件是类型和值都为 nil,这里类型部分是 *MyError(不是 nil),所以 err == nilfalse

修正方案:直接返回 nil 而不是带类型的 nil 指针:

func getError(shouldFail bool) error {
    if !shouldFail {
        return nil  // 直接返回 nil,接口值的类型和值都是 nil
    }
    return &MyError{Msg: "出错了"}
}

关键知识点:当需要返回 nil 错误时,应该直接返回 nil 字面量,而不是一个有类型的 nil 指针。这是 Go 中最常见的接口陷阱之一。

搜索