404 Not Found

404 Not Found


nginx

接口

第9课:接口(Interface)

生活中的类比

想象你去餐厅点餐。你不需要知道厨师是谁、用什么锅、怎么炒菜——你只需要看菜单,点一道"宫保鸡丁"。菜单就是一个接口:它定义了"能做什么",而不关心"谁来做"和"怎么做"。

在Go语言中,接口也是如此。它定义了一组方法的签名,任何类型只要实现了这些方法,就自动满足这个接口——不需要显式声明。


核心概念

概念 说明
接口(Interface) 一组方法签名的集合,定义行为契约
隐式实现 类型实现了接口的所有方法,就自动满足该接口
鸭子类型 "如果它走起来像鸭子、叫起来像鸭子,那它就是鸭子"
空接口 interface{} 不包含任何方法,任何类型都满足它
类型断言 将接口值还原为具体类型
接口组合 通过嵌入多个接口构建更大的接口

基本语法与用法

定义接口

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

隐式实现

GO
// Dog 类型实现了 Speaker 接口(无需声明)
type Dog struct {
    Name string
}

func (d Dog) Speak() string {
    return "汪汪!我是" + d.Name
}

// Cat 类型也实现了 Speaker 接口
type Cat struct {
    Name string
}

func (c Cat) Speak() string {
    return "喵喵!我是" + c.Name
}
💡 提示:Go中没有 implements 关键字。只要一个类型拥有接口要求的所有方法,它就自动实现了该接口。

使用接口

GO
func makeItSpeak(s Speaker) {
    fmt.Println(s.Speak())
}

func main() {
    dog := Dog{Name: "旺财"}
    cat := Cat{Name: "咪咪"}

    makeItSpeak(dog) // 输出:汪汪!我是旺财
    makeItSpeak(cat) // 输出:喵喵!我是咪咪
}

空接口 interface{}

GO
// 空接口可以接收任意类型的值
func printAnything(v interface{}) {
    fmt.Printf("值: %v, 类型: %T\n", v, v)
}

func main() {
    printAnything(42)         // 值: 42, 类型: int
    printAnything("hello")   // 值: hello, 类型: string
    printAnything(3.14)      // 值: 3.14, 类型: float64
}
💡 提示:Go 1.18+ 中 interface{} 可以简写为 any,它们是等价的。

类型断言与类型Switch

GO
func describe(v interface{}) {
    // 类型断言:尝试将接口值转换为具体类型
    str, ok := v.(string)
    if ok {
        fmt.Println("这是一个字符串:", str)
        return
    }

    // 类型 switch:优雅地处理多种类型
    switch val := v.(type) {
    case int:
        fmt.Println("这是一个整数:", val)
    case float64:
        fmt.Println("这是一个浮点数:", val)
    case bool:
        fmt.Println("这是一个布尔值:", val)
    default:
        fmt.Printf("未知类型: %T\n", val)
    }
}
💡 提示:类型断言使用"comma ok"模式可以避免 panic。v.(Type) 如果断言失败会 panic,而 v, ok := v.(Type) 则安全返回零值和 false

接口组合

GO
// 基础接口
type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

// 组合接口:嵌入多个接口
type ReadWriter interface {
    Reader
    Writer
}

// ReadWriter 同时需要实现 Read 和 Write 方法
💡 提示:接口组合遵循"小接口"原则。Go标准库中很多接口都只有1-2个方法,如 io.Readerio.Writerfmt.Stringer 等。


示例

示例:形状面积计算(难度⭐)

GO
package main

import (
    "fmt"
    "math"
)

// Shape 接口定义了"形状"的行为
type Shape interface {
    Area() float64
    Perimeter() float64
}

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

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

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

// Circle 圆形
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
}

// printShapeInfo 接受任何 Shape 接口的实现
func printShapeInfo(s Shape) {
    fmt.Printf("面积: %.2f, 周长: %.2f\n", s.Area(), s.Perimeter())
}

func main() {
    rect := Rectangle{Width: 10, Height: 5}
    circle := Circle{Radius: 7}

    fmt.Print("矩形 -> ")
    printShapeInfo(rect)

    fmt.Print("圆形 -> ")
    printShapeInfo(circle)
}
▶ 试一试
矩形 -> 面积: 50.00, 周长: 30.00
圆形 -> 面积: 153.94, 周长: 43.98

示例:接口切片与排序(难度⭐⭐)

GO
package main

import (
    "fmt"
    "sort"
)

// Employee 员工接口
type Employee interface {
    Name() string
    Salary() float64
}

// FullTime 全职员工
type FullTime struct {
    name   string
    annual float64 // 年薪
}

func (f FullTime) Name() string    { return f.name }
func (f FullTime) Salary() float64 { return f.annual }

// Contractor 合同工
type Contractor struct {
    name    string
    hourly  float64 // 时薪
    hours   float64 // 工作小时数
}

func (c Contractor) Name() string    { return c.name }
func (c Contractor) Salary() float64 { return c.hourly * c.hours }

// BySalary 实现 sort.Interface,按薪资排序
type BySalary []Employee

func (s BySalary) Len() int           { return len(s) }
func (s BySalary) Less(i, j int) bool { return s[i].Salary() < s[j].Salary() }
func (s BySalary) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }

// totalCost 计算总人力成本
func totalCost(employees []Employee) float64 {
    total := 0.0
    for _, e := range employees {
        total += e.Salary()
    }
    return total
}

func main() {
    team := []Employee{
        FullTime{name: "张三", annual: 120000},
        Contractor{name: "李四", hourly: 200, hours: 1000},
        FullTime{name: "王五", annual: 150000},
        Contractor{name: "赵六", hourly: 180, hours: 800},
    }

    fmt.Println("=== 薪资排序前 ===")
    for _, e := range team {
        fmt.Printf("  %s: ¥%.0f\n", e.Name(), e.Salary())
    }

    sort.Sort(BySalary(team))

    fmt.Println("\n=== 薪资排序后 ===")
    for _, e := range team {
        fmt.Printf("  %s: ¥%.0f\n", e.Name(), e.Salary())
    }

    fmt.Printf("\n总人力成本: ¥%.0f\n", totalCost(team))
}
▶ 试一试
=== 薪资排序前 ===
  张三: ¥120000
  李四: ¥200000
  王五: ¥150000
  赵六: ¥144000

=== 薪资排序后 ===
  张三: ¥120000
  赵六: ¥144000
  王五: ¥150000
  李四: ¥200000

总人力成本: ¥614000

示例:实现 io.Reader/Writer 接口(难度⭐⭐⭐)

GO
package main

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

// UpperReader 将读取的内容转为大写
type UpperReader struct {
    source io.Reader
}

// 实现 io.Reader 接口
func (u *UpperReader) Read(p []byte) (n int, err error) {
    n, err = u.source.Read(p)
    // 将读取到的字节全部转为大写
    for i := 0; i < n; i++ {
        if p[i] >= 'a' && p[i] <= 'z' {
            p[i] = p[i] - 32 // ASCII: 小写转大写
        }
    }
    return
}

// UpperReader 的构造函数
func NewUpperReader(r io.Reader) *UpperReader {
    return &UpperReader{source: r}
}

// PrefixWriter 在每次写入前添加前缀
type PrefixWriter struct {
    prefix string
    target io.Writer
}

// 实现 io.Writer 接口
func (p *PrefixWriter) Write(data []byte) (n int, err error) {
    // 先写入前缀
    _, err = p.target.Write([]byte(p.prefix))
    if err != nil {
        return 0, err
    }
    // 再写入实际数据
    return p.target.Write(data)
}

// PrefixWriter 的构造函数
func NewPrefixWriter(prefix string, w io.Writer) *PrefixWriter {
    return &PrefixWriter{prefix: prefix, target: w}
}

// TeeReader 同时读取并写入(类似 tee 命令)
func TeeReader(r io.Reader, w io.Writer) io.Reader {
    return &teeReader{r: r, w: w}
}

type teeReader struct {
    r io.Reader
    w io.Writer
}

func (t *teeReader) Read(p []byte) (n int, err error) {
    n, err = t.r.Read(p)
    if n > 0 {
        // 读取的同时写入到 w
        t.w.Write(p[:n])
    }
    return
}

func main() {
    fmt.Println("=== UpperReader 示例 ===")
    // 从字符串创建 Reader
    source := strings.NewReader("hello, go interfaces!")
    upper := NewUpperReader(source)

    // 使用 io.ReadAll 读取全部内容
    buf := make([]byte, 64)
    n, _ := upper.Read(buf)
    fmt.Printf("大写结果: %s\n", string(buf[:n]))

    fmt.Println("\n=== PrefixWriter 示例 ===")
    // 写入到标准输出,每行添加前缀
    writer := NewPrefixWriter("[LOG] ", &strings.Builder{})
    writer.Write([]byte("系统启动完成\n"))
    // 这里用 strings.Builder 来捕获输出
    var builder strings.Builder
    pw := NewPrefixWriter("[DEBUG] ", &builder)
    pw.Write([]byte("接口初始化成功"))
    fmt.Println(builder.String())

    fmt.Println("\n=== TeeReader 示例 ===")
    // 同时读取并写入到另一个 Writer
    input := strings.NewReader("Go语言很强大")
    var capture strings.Builder
    tee := TeeReader(input, &capture)

    buf2 := make([]byte, 1024)
    n2, _ := tee.Read(buf2)
    fmt.Printf("读取到: %s\n", string(buf2[:n2]))
    fmt.Printf("同时捕获到: %s\n", capture.String())
}
▶ 试一试
=== UpperReader 示例 ===
大写结果: HELLO, GO INTERFACES!

=== PrefixWriter 示例 ===
[DEBUG] 接口初始化成功

=== TeeReader 示例 ===
读取到: Go语言很强大
同时捕获到: Go语言很强大

实际应用场景

场景一:日志系统(策略模式)

GO
package main

import (
    "fmt"
    "os"
    "time"
)

// Logger 日志接口
type Logger interface {
    Log(message string)
}

// ConsoleLogger 控制台日志
type ConsoleLogger struct{}

func (c ConsoleLogger) Log(message string) {
    timestamp := time.Now().Format("2006-01-02 15:04:05")
    fmt.Printf("[%s] %s\n", timestamp, message)
}

// FileLogger 文件日志
type FileLogger struct {
    file *os.File
}

func NewFileLogger(filename string) (*FileLogger, error) {
    f, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
    if err != nil {
        return nil, err
    }
    return &FileLogger{file: f}, nil
}

func (f *FileLogger) Log(message string) {
    timestamp := time.Now().Format("2006-01-02 15:04:05")
    fmt.Fprintf(f.file, "[%s] %s\n", timestamp, message)
}

// MultiLogger 同时输出到多个 Logger
type MultiLogger struct {
    loggers []Logger
}

func (m *MultiLogger) Add(l Logger) {
    m.loggers = append(m.loggers, l)
}

func (m *MultiLogger) Log(message string) {
    for _, l := range m.loggers {
        l.Log(message)
    }
}

// App 使用 Logger 接口,不关心具体实现
type App struct {
    logger Logger
}

func (a *App) Run() {
    a.logger.Log("应用启动")
    a.logger.Log("处理请求中...")
    a.logger.Log("请求处理完成")
}

func main() {
    // 组合多个日志输出
    multi := &MultiLogger{}
    multi.Add(ConsoleLogger{})

    // 可以轻松切换或添加日志输出方式
    app := &App{logger: multi}
    app.Run()
}
[2026-06-26 10:30:00] 应用启动
[2026-06-26 10:30:00] 处理请求中...
[2026-06-26 10:30:00] 请求处理完成

场景二:数据存储抽象层

GO
package main

import "fmt"

// Store 存储接口
type Store interface {
    Get(key string) (string, bool)
    Set(key string, value string)
    Delete(key string)
    Keys() []string
}

// MemoryStore 内存存储实现
type MemoryStore struct {
    data map[string]string
}

func NewMemoryStore() *MemoryStore {
    return &MemoryStore{data: make(map[string]string)}
}

func (m *MemoryStore) Get(key string) (string, bool) {
    val, ok := m.data[key]
    return val, ok
}

func (m *MemoryStore) Set(key string, value string) {
    m.data[key] = value
}

func (m *MemoryStore) Delete(key string) {
    delete(m.data, key)
}

func (m *MemoryStore) Keys() []string {
    keys := make([]string, 0, len(m.data))
    for k := range m.data {
        keys = append(keys, k)
    }
    return keys
}

// CacheService 使用 Store 接口,与具体存储解耦
type CacheService struct {
    store Store
}

func (c *CacheService) GetOrSet(key, defaultValue string) string {
    if val, ok := c.store.Get(key); ok {
        return val
    }
    c.store.Set(key, defaultValue)
    return defaultValue
}

func (c *CacheService) GetAll() map[string]string {
    result := make(map[string]string)
    for _, key := range c.store.Keys() {
        if val, ok := c.store.Get(key); ok {
            result[key] = val
        }
    }
    return result
}

func main() {
    // 使用内存存储
    store := NewMemoryStore()
    cache := &CacheService{store: store}

    // 写入数据
    cache.GetOrSet("user:1", "张三")
    cache.GetOrSet("user:2", "李四")
    cache.GetOrSet("config:theme", "dark")

    // 读取数据
    fmt.Println("所有缓存数据:")
    for k, v := range cache.GetAll() {
        fmt.Printf("  %s = %s\n", k, v)
    }

    // 测试 GetOrSet:已存在的键返回旧值
    result := cache.GetOrSet("user:1", "王五")
    fmt.Printf("\nuser:1 的值: %s\n", result)
}
所有缓存数据:
  user:1 = 张三
  user:2 = 李四
  config:theme = dark

user:1 的值: 张三

📖 小节

要点 说明
接口定义行为 只关心"能做什么",不关心"是什么"
隐式实现 无需声明,实现了方法就满足接口
空接口 any 可以持有任意类型的值
类型断言 从接口值中提取具体类型,使用 comma ok 模式更安全
接口组合 通过嵌入小接口构建大接口
面向接口编程 依赖接口而非具体实现,提高代码灵活性
💡 最佳实践:Go推崇小接口。标准库中最常用的接口通常只有1-2个方法。定义接口时,从消费者(调用方)的需求出发,而不是从实现者出发。


❓ 常见问题

Q1: 接口和结构体有什么区别?

结构体是具体的数据类型,定义了"是什么";接口是行为契约,定义了"能做什么"。结构体可以被实例化,接口不能直接实例化,但可以持有实现了该接口的任何类型的值。

Q2: 为什么Go不需要 implements 关键字?

Go采用鸭子类型设计。编译器会在编译时自动检查类型是否满足接口。这种设计使得接口和实现完全解耦——你可以在不修改现有代码的情况下,为第三方库的类型定义新的接口。

Q3: 什么时候应该定义接口?

💡 经验法则:先写具体实现,当发现需要抽象时再定义接口。不要过度设计。

Q4: interface{}any 有什么区别?

没有区别。any 是Go 1.18引入的 interface{} 的类型别名,两者完全等价。推荐使用 any,因为它更简洁。


📝 作业

练习1:基础 — 实现 Stringer 接口

fmt.Stringer 接口只有一个方法 String() string。当使用 fmt.Println%v 格式化时,Go会自动调用此方法。

GO
// 请为以下类型实现 fmt.Stringer 接口
type Temperature struct {
    Celsius float64
}

type Money struct {
    Amount   float64
    Currency string
}

// 目标效果:
// fmt.Println(Temperature{36.5})  -> "36.5°C"
// fmt.Println(Money{99.9, "CNY"}) -> "¥99.90"

练习2:进阶 — 设计通知系统

设计一个通知系统,支持多种通知方式:

GO
// 1. 定义 Notifier 接口
// 2. 实现 EmailNotifier、SMSNotifier、WechatNotifier
// 3. 实现一个函数,可以同时发送到多个 Notifier
// 4. 使用接口切片存储不同的 Notifier

练习3:挑战 — 实现简单的插件系统

GO
// 定义 Plugin 接口,包含 Name()、Version()、Execute() 方法
// 实现至少3个不同的 Plugin
// 创建 PluginManager,可以注册、查找和执行插件
// 提示:使用 map[string]Plugin 存储插件

下一课

接口是Go语言多态的基石。掌握了接口,你就能编写灵活、可测试、可扩展的代码。接下来,我们将学习Go的错误处理机制——这是Go语言最重要的设计哲学之一。

👉 下一课:错误处理(Error Handling)

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏