Go学习第8天:接口 + 泛型 + 错误处理

Go :接口、泛型、错误处理

  • 一、接口(Interface)
    • [1.1 核心特性](#1.1 核心特性)
    • [1.2 基础语法与使用](#1.2 基础语法与使用)
      • [1.2.1 接口定义](#1.2.1 接口定义)
      • [1.2.2 接口实现](#1.2.2 接口实现)
      • [1.2.3 多态演示](#1.2.3 多态演示)
    • [1.3 空接口 interface{}](#1.3 空接口 interface{})
    • [1.4 接口组合](#1.4 接口组合)
    • [1.5 类型断言 & 类型选择](#1.5 类型断言 & 类型选择)
      • [1.5.1 基础类型断言(两种写法)](#1.5.1 基础类型断言(两种写法))
      • [1.5.2 类型选择(type switch)](#1.5.2 类型选择(type switch))
    • [1.6 接口高频踩坑](#1.6 接口高频踩坑)
  • 二、泛型(Generics)
    • [2.1 核心概念](#2.1 核心概念)
    • [2.2 基础语法与内置约束](#2.2 基础语法与内置约束)
      • [2.2.1 通用语法](#2.2.1 通用语法)
      • [2.2.2 三大内置约束](#2.2.2 三大内置约束)
      • [2.2.3 示例演示](#2.2.3 示例演示)
        • [示例1:any 约束(任意类型通用函数)](#示例1:any 约束(任意类型通用函数))
        • [示例2:comparable 约束(可比较类型)](#示例2:comparable 约束(可比较类型))
        • [示例3 自定义联合约束(限定数字类型)](#示例3 自定义联合约束(限定数字类型))
    • [2.3 常用泛型实战案例](#2.3 常用泛型实战案例)
    • [2.4 泛型高频踩坑](#2.4 泛型高频踩坑)
  • 三、错误处理
    • [3.1 基础:error 接口](#3.1 基础:error 接口)
      • [3.1.1 创建普通错误](#3.1.1 创建普通错误)
      • [3.1.2 函数返回错误(标准写法)](#3.1.2 函数返回错误(标准写法))
    • [3.2 自定义错误类型](#3.2 自定义错误类型)
    • [3.3 错误包装与解析(Go 1.13+)](#3.3 错误包装与解析(Go 1.13+))
      • [3.3.1 错误包装](#3.3.1 错误包装)
      • [3.3.2 错误解析](#3.3.2 错误解析)
    • [3.4 panic & recover(处理致命错误)](#3.4 panic & recover(处理致命错误))
    • [3.5 错误处理高频踩坑](#3.5 错误处理高频踩坑)
  • 四、知识点速记表

目录

  1. 接口(Interface)
  2. 泛型(Generics)
  3. 错误处理(Error、panic、recover)
  4. 知识点速记

一、接口(Interface)

接口是 Go 实现行为契约、多态、解耦 的核心特性,它只定义一组方法签名,不包含字段与方法实现。Go 采用隐式实现,无需关键字声明实现关系,只要类型实现接口全部方法,就默认适配该接口。

1.1 核心特性

  1. 隐式实现 :无 implements 关键字,实现接口所有方法即代表实现接口;
  2. 接口变量 :存储动态类型 (实际类型)+动态值 ,零值为 nil
  3. 空接口 interface{}:可接收任意类型,是所有类型的父集;
  4. 接口组合:支持嵌套多个接口,实现接口继承效果;
  5. 多态:同一接口变量可绑定不同实现类型,调用同名方法执行不同逻辑。

1.2 基础语法与使用

1.2.1 接口定义

语法格式:

go 复制代码
type 接口名 interface {
    方法名1(参数列表) 返回值列表
    方法名2(参数列表) 返回值列表
    // ... 多个方法
}

示例(图形面积、周长接口):

go 复制代码
package main
import "math"
import "fmt"

// 定义 Shape 接口,约定两个行为:求面积、周长
type Shape interface {
    Area() float64
    Perimeter() float64
}

1.2.2 接口实现

任意自定义类型(常用结构体)实现接口全部方法,即自动实现接口。

go 复制代码
// 圆形结构体
type Circle struct {
    Radius float64
}

// 实现 Shape 接口的 Area 方法
func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

// 实现 Shape 接口的 Perimeter 方法
func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.Radius
}

func main() {
    var s Shape   // 定义接口类型变量
    c := Circle{Radius: 5}
    s = c         // 结构体实例赋值给接口变量(多态)
    fmt.Println("面积:", s.Area())
    fmt.Println("周长:", s.Perimeter())
}

1.2.3 多态演示

不同结构体实现同一接口,接口变量切换实例,执行不同逻辑:

go 复制代码
package main
import "fmt"

// 手机接口
type Phone interface {
    call()
}

// 诺基亚结构体
type NokiaPhone struct{}
func (n NokiaPhone) call() {
    fmt.Println("诺基亚:拨打电话")
}

// 苹果手机结构体
type IPhone struct{}
func (i IPhone) call() {
    fmt.Println("iPhone:拨打电话")
}

func main() {
    var p Phone
    p = NokiaPhone{}
    p.call()
    p = IPhone{}
    p.call()
}

1.3 空接口 interface{}

空接口没有定义任何方法 ,因此 Go 中所有类型都默认实现空接口,常用于通用参数、通用容器。

示例:通用打印函数

go 复制代码
package main
import "fmt"

// 接收任意类型参数
func printValue(val interface{}) {
    fmt.Printf("值:%v,类型:%T\n", val, val)
}

func main() {
    printValue(100)
    printValue("Go语言")
    printValue(3.14)
    printValue([]int{1,2,3})
}

1.4 接口组合

将多个接口嵌套,组合成新接口,实现接口复用:

go 复制代码
package main
import "fmt"

type Reader interface {
    Read() string
}
type Writer interface {
    Write(data string)
}

// 组合接口:同时拥有读、写能力
type ReadWriter interface {
    Reader
    Writer
}

// 文件结构体,同时实现两个子接口
type File struct{}
func (f File) Read() string {
    return "读取文件内容"
}
func (f File) Write(data string) {
    fmt.Println("写入内容:", data)
}

func main() {
    var rw ReadWriter = File{}
    fmt.Println(rw.Read())
    rw.Write("测试数据")
}

1.5 类型断言 & 类型选择

接口变量存储的是抽象类型,如需还原底层具体类型,使用类型断言 ;批量判断类型使用 type switch

1.5.1 基础类型断言(两种写法)

  1. 直接断言(失败触发 panic,不推荐)

    go 复制代码
    var i interface{} = "测试字符串"
    str := i.(string)
  2. 安全断言(搭配 ok 判断,工程首选)

    go 复制代码
    package main
    import "fmt"
    
    func main() {
        var i interface{} = 123
        str, ok := i.(string)
        if ok {
            fmt.Println("断言成功:", str)
        } else {
            fmt.Println("类型不匹配")
        }
    }

1.5.2 类型选择(type switch)

批量匹配接口底层类型,适合多类型分支判断:

go 复制代码
package main
import "fmt"

func checkType(val interface{}) {
    switch v := val.(type) {
    case int:
        fmt.Println("整型:", v)
    case string:
        fmt.Println("字符串:", v)
    case float64:
        fmt.Println("浮点型:", v)
    default:
        fmt.Println("未知类型")
    }
}

func main() {
    checkType(666)
    checkType("接口测试")
}

1.6 接口高频踩坑

  1. 方法未全部实现:结构体只实现接口部分方法,编译报错;
  2. 值接收 vs 指针接收 :接口方法如果是指针接收者,只能赋值结构体指针,不能赋值结构体实例;
  3. 空接口误用 :滥用 interface{} 会丢失类型安全,尽量优先具体类型;
  4. 断言不做判断 :直接断言不使用 ok,类型不匹配直接程序崩溃;
  5. nil 接口判断 :接口变量 = nil 的条件是动态类型、动态值全部为 nil,仅值为 nil 时判断结果为 false。

二、泛型(Generics)

泛型是 Go 1.18 正式引入的特性,用于编写类型无关、可复用 的通用代码,避免为不同类型重复编写相同逻辑。核心组成:类型参数 + 类型约束

2.1 核心概念

  1. 类型参数 :函数/结构体后用 [] 声明占位类型,如 [T]
  2. 类型约束:限制类型参数的可用类型(如任意类型、可比较类型、自定义类型集合);
  3. 类型推断:多数场景编译器可自动推导类型,无需显式声明。

2.2 基础语法与内置约束

2.2.1 通用语法

泛型函数
go 复制代码
func 函数名[T 约束](参数列表) 返回值 { 函数体 }
泛型结构体
go 复制代码
type 结构体名[T 约束] struct { 字段 }

2.2.2 三大内置约束

约束 含义 适用场景
any 等价 interface{},允许任意类型 不限制类型的通用工具
comparable 允许使用 ==/!= 比较的类型 Map 键、元素查找、去重
自定义联合约束 用 ` ` 组合多种具体类型

2.2.3 示例演示

示例1:any 约束(任意类型通用函数)
go 复制代码
package main
import "fmt"

// T 为任意类型
func PrintAny[T any](val T) {
    fmt.Printf("值:%v,类型:%T\n", val)
}

func main() {
    PrintAny(10)
    PrintAny("泛型测试")
    PrintAny(3.14)
}
示例2:comparable 约束(可比较类型)

实现通用切片元素查找:

go 复制代码
package main
import "fmt"

// 仅支持可比较类型
func FindIndex[T comparable](slice []T, target T) int {
    for idx, v := range slice {
        if v == target {
            return idx
        }
    }
    return -1
}

func main() {
    nums := []int{1,2,3}
    fmt.Println(FindIndex(nums,2)) // 输出 1
}
示例3 自定义联合约束(限定数字类型)
go 复制代码
package main
import "fmt"

// 自定义数字约束
type Number interface {
    int | int8 | int16 | int32 | int64 |
    uint | float32 | float64
}

// 通用加法函数
func Add[T Number](a, b T) T {
    return a + b
}

func main() {
    fmt.Println(Add(10,20))
    fmt.Println(Add(1.5,2.5))
}

2.3 常用泛型实战案例

案例1:通用工具函数(交换、去重、判断包含)

go 复制代码
package main
import "fmt"

// 交换两个任意类型变量
func Swap[T any](a, b T) (T, T) {
    return b, a
}

// 切片去重
func Unique[T comparable](slice []T) []T {
    m := make(map[T]bool)
    var res []T
    for _, v := range slice {
        if !m[v] {
            m[v] = true
            res = append(res, v)
        }
    }
    return res
}

func main() {
    a, b := 1, 2
    a, b = Swap(a, b)
    fmt.Println(a, b)

    old := []int{1,1,2,2,3}
    fmt.Println(Unique(old))
}

案例2:泛型结构体(通用栈)

go 复制代码
package main
import "fmt"

// 泛型栈
type Stack[T any] struct {
    elements []T
}

// 入栈
func (s *Stack[T]) Push(val T) {
    s.elements = append(s.elements, val)
}
// 出栈
func (s *Stack[T]) Pop() (T, bool) {
    if len(s.elements) == 0 {
        var zero T
        return zero, false
    }
    idx := len(s)-1
    val := s.elements[idx]
    s.elements = s.elements[:idx]
    return val, true
}

func main() {
    // 整型栈
    intStack := Stack[int]{}
    intStack.Push(10)
    fmt.Println(intStack.Pop())
}

2.4 泛型高频踩坑

  1. 版本限制:泛型仅 Go 1.18 及以上版本支持,低版本直接编译报错;
  2. 约束误用 :对非可比较类型(切片、Map)使用 comparable 约束,编译失败;
  3. 类型推断失效 :部分复杂场景需手动指定泛型类型 函数名[类型](参数)
  4. 过度使用泛型:简单逻辑无需泛型,增加代码可读性负担;
  5. 方法约束:泛型类型的方法不能再额外添加类型参数。

三、错误处理

Go 不使用传统 try-catch 异常机制,采用显式错误返回 为核心,搭配 panic(致命恐慌)和 recover(恢复恐慌)处理严重异常,分为三类场景:普通业务错误、运行恐慌、自定义错误。

3.1 基础:error 接口

error 是 Go 内置接口,所有错误类型都必须实现该接口:

go 复制代码
type error interface {
    Error() string // 返回错误描述文本
}

3.1.1 创建普通错误

使用标准库 errors.New 创建基础错误:

go 复制代码
package main
import "errors"
import "fmt"

func main() {
    err := errors.New("参数非法")
    fmt.Println(err)
}

3.1.2 函数返回错误(标准写法)

业务函数通常将 error 作为最后一个返回值nil 代表无错误:

go 复制代码
package main
import "errors"
import "fmt"

// 除法函数,除数为0返回错误
func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("除数不能为0")
    }
    return a / b, nil
}

func main() {
    res, err := divide(10, 0)
    if err != nil { // 优先判断错误
        fmt.Println("执行失败:", err)
        return
    }
    fmt.Println("结果:", res)
}

3.2 自定义错误类型

通过结构体实现 error 接口,扩展错误信息(错误码、详情等):

go 复制代码
package main
import "fmt"

// 自定义除法错误
type DivideErr struct {
    Dividend int // 被除数
    Divisor  int // 除数
}

// 实现 error 接口
func (e *DivideErr) Error() string {
    return fmt.Sprintf("错误:%d 不能除以 %d", e.Dividend, e.Divisor)
}

func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, &DivideErr{Dividend: a, Divisor: b}
    }
    return a / b, nil
}

func main() {
    _, err := divide(20, 0)
    fmt.Println(err)
}

3.3 错误包装与解析(Go 1.13+)

3.3.1 错误包装

使用 fmt.Errorf("%w", 原始错误) 包装错误,保留原始错误链:

go 复制代码
package main
import "errors"
import "fmt"

var ErrNotFound = errors.New("数据不存在")

func queryData(id int) error {
    // 包装原始错误,追加上下文
    return fmt.Errorf("查询id=%d 失败:%w", id, ErrNotFound)
}

3.3.2 错误解析

  1. errors.Is:判断错误链中是否包含指定原始错误;
  2. errors.As:将错误转为自定义错误类型。
go 复制代码
package main
import "errors"
import "fmt"

var ErrNotFound = errors.New("数据不存在")

func queryData(id int) error {
    return fmt.Errorf("查询id=%d 失败:%w", id, ErrNotFound)
}

func main() {
    err := queryData(1001)
    // 判断是否是指定错误
    if errors.Is(err, ErrNotFound) {
        fmt.Println("根错误:数据不存在")
    }
}

3.4 panic & recover(处理致命错误)

  • panic :主动抛出运行恐慌,程序停止正常执行,逐层执行 defer;用于不可恢复的严重错误(如配置加载失败、核心依赖缺失);
  • recover :搭配 defer 使用,捕获 panic,恢复程序运行。

标准使用模板

go 复制代码
package main
import "fmt"

func safeFunc() {
    // 必须在 defer 匿名函数内调用 recover
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("捕获恐慌:", r)
        }
    }()
    // 触发恐慌
    panic("数组下标越界")
}

func main() {
    fmt.Println("程序开始")
    safeFunc()
    fmt.Println("程序继续执行") // 未崩溃,正常运行
}

3.5 错误处理高频踩坑

  1. 忽略错误返回值 :函数返回 error 时不判断,隐藏线上隐患;
  2. 错误判断顺序错误:先使用结果、再判断错误(错误时结果为零值);
  3. recover 使用错误recover 必须放在 defer 匿名函数内,嵌套/后置均无法捕获;
  4. 滥用 panic :普通业务错误使用 errorpanic 仅用于致命场景;
  5. 错误包装丢失根错误 :包装错误必须使用 %w,否则无法用 errors.Is/As 解析。

四、知识点速记表

模块 核心要点 高频踩坑
接口 隐式实现、空接口接收任意类型、类型断言/type switch、接口组合 方法实现不全、指针/值接收混淆、直接断言不判空
泛型 类型参数+约束、any/comparable/自定义约束、泛型函数/结构体 Go1.18以下不支持、约束类型不匹配、过度泛型
错误处理 error 接口、显式返回错误、%w包装错误、panic+recover 忽略error、refer使用位置错误、普通场景滥用panic
相关推荐
fanged1 小时前
高通学习12--调试工具(TODO)
学习
AI小百科1 小时前
成为FDE的系统学习路径(2026版)
人工智能·学习·ai应用
聆风吟º1 小时前
Python基础数据类型(一):数字类型
开发语言·python·float·int·bool·数字类型
小灰灰搞电子1 小时前
C++ boost::container 详解:高性能容器库完全指南
开发语言·c++·boost
Brilliantwxx1 小时前
【C++】 C++11 知识点梳理(上)
开发语言·c++
飞天狗1111 小时前
零基础JavaWeb入门——第4课:表单处理 —— 浏览器怎么把数据发给服务器
java·开发语言·前端·后端·servlet
三品吉他手会点灯1 小时前
STM32F103 学习笔记-24-I2C-读写EEPROM(第4节)-STM32的I2C通讯过程
笔记·stm32·学习
多彩电脑1 小时前
向AIDE(安卓设备上的Android Studio)导入aar库
android·java·开发语言·androidx
江屿风2 小时前
C++图论基础单源最短路-常规版dijkstra算法/堆优化版dijkstra算法/bellman-ford 算法/spfa 算法流食般投喂
开发语言·c++·笔记·算法·图论