Go学习第8天:接口 + 泛型 + 错误处理
Go :接口、泛型、错误处理
- 目录
- 一、接口(Interface)
- 1.1 核心特性
- 1.2 基础语法与使用
- 1.2.1 接口定义
- 1.2.2 接口实现
- 1.2.3 多态演示
- 1.3 空接口 interface{}
- 示例:通用打印函数
- 1.4 接口组合
- 1.5 类型断言 & 类型选择
- 1.5.1 基础类型断言(两种写法)
- 1.5.2 类型选择(type switch)
- 1.6 接口高频踩坑
- 二、泛型(Generics)
- 2.1 核心概念
- 2.2 基础语法与内置约束
- 2.2.1 通用语法
- 泛型函数
- 泛型结构体
- 2.2.2 三大内置约束
- 2.2.3 示例演示
- 示例1:any 约束(任意类型通用函数)
- 示例2:comparable 约束(可比较类型)
- 示例3 自定义联合约束(限定数字类型)
- 2.3 常用泛型实战案例
- 案例1:通用工具函数(交换、去重、判断包含)
- 案例2:泛型结构体(通用栈)
- 2.4 泛型高频踩坑
- 三、错误处理
- 3.1 基础:error 接口
- 3.1.1 创建普通错误
- 3.1.2 函数返回错误(标准写法)
- 3.2 自定义错误类型
- 3.3 错误包装与解析(Go 1.13+)
- 3.3.1 错误包装
- 3.3.2 错误解析
- 3.4 panic & recover(处理致命错误)
- 标准使用模板
- 3.5 错误处理高频踩坑
- 四、知识点速记表
目录
- 接口(Interface)
- 泛型(Generics)
- 错误处理(Error、panic、recover)
- 知识点速记
一、接口(Interface)
接口是 Go 实现行为契约、多态、解耦的核心特性,它只定义一组方法签名,不包含字段与方法实现。Go 采用隐式实现,无需关键字声明实现关系,只要类型实现接口全部方法,就默认适配该接口。
1.1 核心特性
- 隐式实现:无
implements关键字,实现接口所有方法即代表实现接口; - 接口变量:存储动态类型(实际类型)+动态值,零值为
nil; - 空接口
interface{}:可接收任意类型,是所有类型的父集; - 接口组合:支持嵌套多个接口,实现接口继承效果;
- 多态:同一接口变量可绑定不同实现类型,调用同名方法执行不同逻辑。
1.2 基础语法与使用
1.2.1 接口定义
语法格式:
type接口名interface{方法名1(参数列表)返回值列表 方法名2(参数列表)返回值列表// ... 多个方法}示例(图形面积、周长接口):
packagemainimport"math"import"fmt"// 定义 Shape 接口,约定两个行为:求面积、周长typeShapeinterface{Area()float64Perimeter()float64}1.2.2 接口实现
任意自定义类型(常用结构体)实现接口全部方法,即自动实现接口。
// 圆形结构体typeCirclestruct{Radiusfloat64}// 实现 Shape 接口的 Area 方法func(c Circle)Area()float64{returnmath.Pi*c.Radius*c.Radius}// 实现 Shape 接口的 Perimeter 方法func(c Circle)Perimeter()float64{return2*math.Pi*c.Radius}funcmain(){vars Shape// 定义接口类型变量c:=Circle{Radius:5}s=c// 结构体实例赋值给接口变量(多态)fmt.Println("面积:",s.Area())fmt.Println("周长:",s.Perimeter())}1.2.3 多态演示
不同结构体实现同一接口,接口变量切换实例,执行不同逻辑:
packagemainimport"fmt"// 手机接口typePhoneinterface{call()}// 诺基亚结构体typeNokiaPhonestruct{}func(n NokiaPhone)call(){fmt.Println("诺基亚:拨打电话")}// 苹果手机结构体typeIPhonestruct{}func(i IPhone)call(){fmt.Println("iPhone:拨打电话")}funcmain(){varp Phone p=NokiaPhone{}p.call()p=IPhone{}p.call()}1.3 空接口 interface{}
空接口没有定义任何方法,因此 Go 中所有类型都默认实现空接口,常用于通用参数、通用容器。
示例:通用打印函数
packagemainimport"fmt"// 接收任意类型参数funcprintValue(valinterface{}){fmt.Printf("值:%v,类型:%T\n",val,val)}funcmain(){printValue(100)printValue("Go语言")printValue(3.14)printValue([]int{1,2,3})}1.4 接口组合
将多个接口嵌套,组合成新接口,实现接口复用:
packagemainimport"fmt"typeReaderinterface{Read()string}typeWriterinterface{Write(datastring)}// 组合接口:同时拥有读、写能力typeReadWriterinterface{Reader Writer}// 文件结构体,同时实现两个子接口typeFilestruct{}func(f File)Read()string{return"读取文件内容"}func(f File)Write(datastring){fmt.Println("写入内容:",data)}funcmain(){varrw ReadWriter=File{}fmt.Println(rw.Read())rw.Write("测试数据")}1.5 类型断言 & 类型选择
接口变量存储的是抽象类型,如需还原底层具体类型,使用类型断言;批量判断类型使用type switch。
1.5.1 基础类型断言(两种写法)
- 直接断言(失败触发
panic,不推荐)variinterface{}="测试字符串"str:=i.(string) - 安全断言(搭配
ok判断,工程首选)packagemainimport"fmt"funcmain(){variinterface{}=123str,ok:=i.(string)ifok{fmt.Println("断言成功:",str)}else{fmt.Println("类型不匹配")}}
1.5.2 类型选择(type switch)
批量匹配接口底层类型,适合多类型分支判断:
packagemainimport"fmt"funccheckType(valinterface{}){switchv:=val.(type){caseint:fmt.Println("整型:",v)casestring:fmt.Println("字符串:",v)casefloat64:fmt.Println("浮点型:",v)default:fmt.Println("未知类型")}}funcmain(){checkType(666)checkType("接口测试")}1.6 接口高频踩坑
- 方法未全部实现:结构体只实现接口部分方法,编译报错;
- 值接收 vs 指针接收:接口方法如果是指针接收者,只能赋值结构体指针,不能赋值结构体实例;
- 空接口误用:滥用
interface{}会丢失类型安全,尽量优先具体类型; - 断言不做判断:直接断言不使用
ok,类型不匹配直接程序崩溃; - nil 接口判断:接口变量 =
nil的条件是动态类型、动态值全部为 nil,仅值为 nil 时判断结果为 false。
二、泛型(Generics)
泛型是 Go 1.18 正式引入的特性,用于编写类型无关、可复用的通用代码,避免为不同类型重复编写相同逻辑。核心组成:类型参数+类型约束。
2.1 核心概念
- 类型参数:函数/结构体后用
[]声明占位类型,如[T]; - 类型约束:限制类型参数的可用类型(如任意类型、可比较类型、自定义类型集合);
- 类型推断:多数场景编译器可自动推导类型,无需显式声明。
2.2 基础语法与内置约束
2.2.1 通用语法
泛型函数
func函数名[T 约束](参数列表)返回值{函数体}泛型结构体
type结构体名[T 约束]struct{字段}2.2.2 三大内置约束
| 约束 | 含义 | 适用场景 |
|---|---|---|
any | 等价interface{},允许任意类型 | 不限制类型的通用工具 |
comparable | 允许使用==/!=比较的类型 | Map 键、元素查找、去重 |
| 自定义联合约束 | 用 ` | ` 组合多种具体类型 |
2.2.3 示例演示
示例1:any 约束(任意类型通用函数)
packagemainimport"fmt"// T 为任意类型funcPrintAny[T any](val T){fmt.Printf("值:%v,类型:%T\n",val)}funcmain(){PrintAny(10)PrintAny("泛型测试")PrintAny(3.14)}示例2:comparable 约束(可比较类型)
实现通用切片元素查找:
packagemainimport"fmt"// 仅支持可比较类型funcFindIndex[T comparable](slice[]T,target T)int{foridx,v:=rangeslice{ifv==target{returnidx}}return-1}funcmain(){nums:=[]int{1,2,3}fmt.Println(FindIndex(nums,2))// 输出 1}示例3 自定义联合约束(限定数字类型)
packagemainimport"fmt"// 自定义数字约束typeNumberinterface{int|int8|int16|int32|int64|uint|float32|float64}// 通用加法函数funcAdd[T Number](a,b T)T{returna+b}funcmain(){fmt.Println(Add(10,20))fmt.Println(Add(1.5,2.5))}2.3 常用泛型实战案例
案例1:通用工具函数(交换、去重、判断包含)
packagemainimport"fmt"// 交换两个任意类型变量funcSwap[T any](a,b T)(T,T){returnb,a}// 切片去重funcUnique[T comparable](slice[]T)[]T{m:=make(map[T]bool)varres[]Tfor_,v:=rangeslice{if!m[v]{m[v]=trueres=append(res,v)}}returnres}funcmain(){a,b:=1,2a,b=Swap(a,b)fmt.Println(a,b)old:=[]int{1,1,2,2,3}fmt.Println(Unique(old))}案例2:泛型结构体(通用栈)
packagemainimport"fmt"// 泛型栈typeStack[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){iflen(s.elements)==0{varzero Treturnzero,false}idx:=len(s)-1val:=s.elements[idx]s.elements=s.elements[:idx]returnval,true}funcmain(){// 整型栈intStack:=Stack[int]{}intStack.Push(10)fmt.Println(intStack.Pop())}2.4 泛型高频踩坑
- 版本限制:泛型仅 Go 1.18 及以上版本支持,低版本直接编译报错;
- 约束误用:对非可比较类型(切片、Map)使用
comparable约束,编译失败; - 类型推断失效:部分复杂场景需手动指定泛型类型
函数名[类型](参数); - 过度使用泛型:简单逻辑无需泛型,增加代码可读性负担;
- 方法约束:泛型类型的方法不能再额外添加类型参数。
三、错误处理
Go 不使用传统try-catch异常机制,采用显式错误返回为核心,搭配panic(致命恐慌)和recover(恢复恐慌)处理严重异常,分为三类场景:普通业务错误、运行恐慌、自定义错误。
3.1 基础:error 接口
error是 Go 内置接口,所有错误类型都必须实现该接口:
typeerrorinterface{Error()string// 返回错误描述文本}3.1.1 创建普通错误
使用标准库errors.New创建基础错误:
packagemainimport"errors"import"fmt"funcmain(){err:=errors.New("参数非法")fmt.Println(err)}3.1.2 函数返回错误(标准写法)
业务函数通常将error作为最后一个返回值,nil代表无错误:
packagemainimport"errors"import"fmt"// 除法函数,除数为0返回错误funcdivide(a,bint)(int,error){ifb==0{return0,errors.New("除数不能为0")}returna/b,nil}funcmain(){res,err:=divide(10,0)iferr!=nil{// 优先判断错误fmt.Println("执行失败:",err)return}fmt.Println("结果:",res)}3.2 自定义错误类型
通过结构体实现 error 接口,扩展错误信息(错误码、详情等):
packagemainimport"fmt"// 自定义除法错误typeDivideErrstruct{Dividendint// 被除数Divisorint// 除数}// 实现 error 接口func(e*DivideErr)Error()string{returnfmt.Sprintf("错误:%d 不能除以 %d",e.Dividend,e.Divisor)}funcdivide(a,bint)(int,error){ifb==0{return0,&DivideErr{Dividend:a,Divisor:b}}returna/b,nil}funcmain(){_,err:=divide(20,0)fmt.Println(err)}3.3 错误包装与解析(Go 1.13+)
3.3.1 错误包装
使用fmt.Errorf("%w", 原始错误)包装错误,保留原始错误链:
packagemainimport"errors"import"fmt"varErrNotFound=errors.New("数据不存在")funcqueryData(idint)error{// 包装原始错误,追加上下文returnfmt.Errorf("查询id=%d 失败:%w",id,ErrNotFound)}3.3.2 错误解析
- errors.Is:判断错误链中是否包含指定原始错误;
- errors.As:将错误转为自定义错误类型。
packagemainimport"errors"import"fmt"varErrNotFound=errors.New("数据不存在")funcqueryData(idint)error{returnfmt.Errorf("查询id=%d 失败:%w",id,ErrNotFound)}funcmain(){err:=queryData(1001)// 判断是否是指定错误iferrors.Is(err,ErrNotFound){fmt.Println("根错误:数据不存在")}}3.4 panic & recover(处理致命错误)
- panic:主动抛出运行恐慌,程序停止正常执行,逐层执行
defer;用于不可恢复的严重错误(如配置加载失败、核心依赖缺失); - recover:搭配
defer使用,捕获panic,恢复程序运行。
标准使用模板
packagemainimport"fmt"funcsafeFunc(){// 必须在 defer 匿名函数内调用 recoverdeferfunc(){ifr:=recover();r!=nil{fmt.Println("捕获恐慌:",r)}}()// 触发恐慌panic("数组下标越界")}funcmain(){fmt.Println("程序开始")safeFunc()fmt.Println("程序继续执行")// 未崩溃,正常运行}3.5 错误处理高频踩坑
- 忽略错误返回值:函数返回
error时不判断,隐藏线上隐患; - 错误判断顺序错误:先使用结果、再判断错误(错误时结果为零值);
- recover 使用错误:
recover必须放在defer匿名函数内,嵌套/后置均无法捕获; - 滥用 panic:普通业务错误使用
error,panic仅用于致命场景; - 错误包装丢失根错误:包装错误必须使用
%w,否则无法用errors.Is/As解析。
四、知识点速记表
| 模块 | 核心要点 | 高频踩坑 |
|---|---|---|
| 接口 | 隐式实现、空接口接收任意类型、类型断言/type switch、接口组合 | 方法实现不全、指针/值接收混淆、直接断言不判空 |
| 泛型 | 类型参数+约束、any/comparable/自定义约束、泛型函数/结构体 | Go1.18以下不支持、约束类型不匹配、过度泛型 |
| 错误处理 | error 接口、显式返回错误、%w包装错误、panic+recover | 忽略error、refer使用位置错误、普通场景滥用panic |
