一、先理解什么是接口

接口就是定义一组行为(方法),但不关心具体怎么实现。就像“交通工具”这个接口,定义了“能移动”、“能刹车”,至于汽车、自行车怎么实现这些功能,接口不管。

二、Go 接口的特点(跟其他语言不一样)

最关键:Go 的接口是隐式实现的

  • Java、C# 需要显式写 implements

  • Go 不需要!只要一个类型拥有接口要求的所有方法,它就自动实现了该接口

三、底层结构(简单理解)

Go 接口在内存中占 2 个字(16 字节,64 位系统):

  • 第一个字:指向类型信息(这个变量是什么类型)

  • 第二个字:指向数据本身(或者数据指针)

接口变量 = [类型指针] [数据指针]

四、基础例子

package main

import "fmt"

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

// 定义 Dog 结构体
type Dog struct {
    Name string
}

// Dog 实现 Speak 方法(自动就实现了 Speaker 接口)
func (d Dog) Speak() string {
    return "汪汪!我是" + d.Name
}

// 定义 Cat 结构体
type Cat struct {
    Name string
}

// Cat 实现 Speak 方法
func (c Cat) Speak() string {
    return "喵喵!我是" + c.Name
}

func main() {
    var speaker Speaker  // 声明接口变量
    
    speaker = Dog{Name: "旺财"}
    fmt.Println(speaker.Speak())  // 汪汪!我是旺财
    
    speaker = Cat{Name: "咪咪"}
    fmt.Println(speaker.Speak())  // 喵喵!我是咪咪
}

发生了什么?

  1. speaker = Dog{...} 时,Go 自动把 Dog 类型信息 + Dog 数据 存入接口

  2. 调用 speaker.Speak() 时,Go 通过类型指针找到 Dog 的 Speak 方法执行

五、更形象的例子(空的接口)

// interface{} 可以表示任何类型(所有类型都实现了空接口)
var anything interface{}

anything = 42
fmt.Println(anything)  // 42

anything = "hello"
fmt.Println(anything)  // hello

anything = Dog{Name: "小黑"}
fmt.Println(anything.(Dog).Name)  // 小黑(需要类型断言)

六、类型断言(把接口里的数据取出来)

var i interface{} = "hello"

// 方式1:带检查的安全断言
s, ok := i.(string)
if ok {
    fmt.Println("是字符串:", s)
}

// 方式2:不检查(万一失败会 panic)
// s2 := i.(string)

// 方式3:type switch
switch v := i.(type) {
case string:
    fmt.Println("字符串:", v)
case int:
    fmt.Println("整数:", v)
default:
    fmt.Println("其他类型")
}

七、指针接收者 vs 值接收者(重要!)

type Mover interface {
    Move()
}

type Car struct{}

// 值接收者
func (c Car) Move() {
    fmt.Println("汽车在移动")
}

type Bike struct{}

// 指针接收者
func (b *Bike) Move() {
    fmt.Println("自行车在移动")
}

func main() {
    var m Mover
    
    m = Car{}      // ✅ 值可以
    m = &Car{}     // ✅ 指针也可以(Go 自动解引用)
    
    m = Bike{}     // ❌ 编译错误!Bike 没有实现 Move(因为方法需要指针)
    m = &Bike{}    // ✅ 指针可以
    
    // 规则:如果接口方法是值接收者,值和指针都能赋值
    //      如果接口方法是指针接收者,只有指针能赋值
}

八、nil 接口 vs 非 nil 接口

var s1 Speaker   // nil 接口:类型=nil, 数据=nil
fmt.Println(s1 == nil)  // true

var d *Dog = nil
var s2 Speaker = d  // 不是 nil 接口!类型=*Dog, 数据=nil
fmt.Println(s2 == nil)  // false

// 调用会怎样?
// s2.Speak()  // panic!方法内如果访问 d.Name 会空指针

九、底层实现图示

正常接口(非空):
┌──────────────┐
│ 类型指针      │ -> 指向 Dog 的元数据(包含方法表)
├──────────────┤
│ 数据指针      │ -> 指向 Dog 实例 {"旺财"}
└──────────────┘

空接口 interface{}:
┌──────────────┐
│ 类型指针      │ -> 指向数据类型
├──────────────┤
│ 数据指针      │ -> 指向数据
└──────────────┘

十、实际应用场景

// 场景1:标准库的 Stringer 接口
type Person struct {
    Name string
    Age  int
}

func (p Person) String() string {
    return fmt.Sprintf("%s (%d岁)", p.Name, p.Age)
}
// 这样 fmt.Println(person) 就会自动调用 String()

// 场景2:排序接口
type Students []Student
func (s Students) Len() int           { return len(s) }
func (s Students) Less(i, j int) bool { return s[i].Score > s[j].Score }
func (s Students) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
// sort.Sort(students) 就能排序

// 场景3:错误处理
type MyError struct {
    Msg string
}
func (e *MyError) Error() string {
    return e.Msg
}

总结(记住这 4 点)

  1. 隐式实现:不需要写 implements,有方法就自动实现

  2. 鸭子类型:看起来像鸭子,叫起来像鸭子,就是鸭子

  3. 双字结构类型指针(找方法)+ 数据指针(找数据)

  4. 灵活性:解耦调用方和实现方,依赖接口而非具体类型

记忆口诀

接口定行为,类型去实现
不用写继承,有方法就算
底层俩指针,类型和数据
判空要注意,类型非 nil 也不行

更多推荐