行业资讯

Go学习笔记:接口与面向接口编程——Go 最核心的设计

发布时间:2026/8/21 22:27:43
Go学习笔记:接口与面向接口编程——Go 最核心的设计 本文首发于 栏轩·阁欢迎访问阅读原文获取更好的阅读体验。一、什么是接口接口interface定义了一组方法签名方法名、参数、返回值。任何类型只要实现了这些方法就自动隐式满足了这个接口。这是 Go 和 Java/C# 最大的不同——不需要写implements。// 定义一个接口typeSpeakerinterface{Speak()string}// Dog 和 Cat 都没写 implements Speaker// 但只要有 Speak() 方法它们就是 SpeakertypeDogstruct{Namestring}func(d Dog)Speak()string{return汪汪我是 d.Name}typeCatstruct{Namestring}func(c Cat)Speak()string{return喵~ 我是 c.Name}接口怎么用// 接受接口参数的函数任何 Speaker 都能传进来funcgreet(s Speaker){fmt.Println(s.Speak())}greet(Dog{Name:旺财})// 汪汪我是 旺财greet(Cat{Name:咪咪})// 喵~ 我是 咪咪greet函数不关心传进来的是狗还是猫只关心它能不能Speak()。这就是面向接口编程。二、隐式实现Duck TypingGo 的接口实现是隐式的——类型不需要声明实现了哪个接口。// 不需要像 Java 这样写// class Dog implements Speaker { ... }// Go类型有了对应的方法自动就实现了// 编译时检查没实现就通不过编译这就是经典的鸭子类型如果一个东西走起来像鸭子、叫起来像鸭子那它就是鸭子。vars Speaker sDog{Name:旺财}// ✅ Dog 有 Speak()编译通过// s Robot{} // ❌ Robot 没有 Speak()编译错误三、接口匹配规则——必须是复制粘贴级别的一致3.1 匹配条件一个类型要实现接口方法名、参数类型、返回值类型必须完全一致typeSpeakerinterface{Speak()string}下面这些都实现不了Speaker// ❌ 返回值不同func(d Dog)Speak(){fmt.Println(汪汪)}// ❌ 参数不同func(d Dog)Speak(volumeint)string{return汪汪}// ❌ 方法名不同func(d Dog)Bark()string{return汪汪}// ✅ 完全匹配func(d Dog)Speak()string{return汪汪}3.2 匹配规则表条件要求方法名必须一模一样参数类型个数、顺序、类型都必须一致返回值类型个数、顺序、类型都必须一致参数名不要求只看类型重点是最后一条——参数名不影响匹配typeSpeakerinterface{Speak(messagestring)string}// 参数名叫 msg 也可以不一定要叫 messagefunc(d Dog)Speak(msgstring)string{returnmsg}// 只看类型(string) string 对上了 ✅ 接口规定的是签名形状signature不是参数名字。四、类型断言有了接口后有时候需要把接口还原为原始类型。这叫类型断言。vars SpeakerDog{Name:旺财}// 语法s.(类型)dog:s.(Dog)// 从接口中取出 Dogfmt.Println(dog.Name)// 现在可以访问 Dog 特有的字段了安全的断言带 ok// 不带 ok如果猜错了程序会 panic崩溃// dog : s.(Cat) // ❌ panic// 带 ok安全猜错也不会崩溃cat,ok:s.(Cat)ifok{fmt.Println(是猫:,cat.Name)}else{fmt.Println(不是猫)}四、类型 switchGo 为类型断言提供了专门的 switch 语法switch v : x.(type)这在需要根据接口的实际类型做不同处理时非常有用。funcdescribe(s Speaker){switchv:s.(type){caseDog:fmt.Printf( 狗名字: %s\n,v.Name)caseCat:fmt.Printf( 猫名字: %s\n,v.Name)default:fmt.Println(❓ 未知类型)}}类型 switch 的特点特性说明v : x.(type)特殊写法只能在 switch 中使用case Dog:匹配具体类型default:兜底变量v在每个 case 中自动转换为对应类型与普通 switch 的区别// 普通 switch匹配值switchx{case1:...case2:...}// 类型 switch匹配类型switchv:x.(type){caseint:fmt.Println(整数:,v)casestring:fmt.Println(字符串:,v)} 每个 case 里的v自动变成了对应的类型不需要再做类型断言。六、空接口 / any空接口就是没有任何方法要求的接口所以任何类型都实现了它// 两种写法等价varv1interface{}varv2 any// Go 1.18 推荐用 anyfuncprintValue(v any){fmt.Printf(值: %v, 类型: %T\n,v,v)}printValue(42)// 值: 42, 类型: intprintValue(hello)// 值: hello, 类型: stringprintValue(3.14)// 值: 3.14, 类型: float64printValue(Dog{})// 值: {}, 类型: main.Dog什么时候用 any日志记录、序列化等需要处理任意类型的场景容器类型如json.Marshal接受any不要滥用——能用具体接口就用具体接口用any会失去编译时类型检查七、接口嵌套组合Go 的接口可以通过嵌套来组合多个接口typeSpeakerinterface{Speak()string}typeRunnerinterface{Run()string}// 组合成新的接口typeAnimalinterface{Speaker Runner}Go 推荐**组合composition**而不是继承inheritance。小接口组合成大接口比一个大而全的接口更灵活。// 只依赖 Speaker 的函数funcgreet(s Speaker){...}// 只依赖 Runner 的函数funcrace(r Runner){...}// 需要两者funcshow(a Animal){fmt.Println(a.Speak())fmt.Println(a.Run())}八、接口值的本质一个接口变量在底层存的是两个东西┌─────────────┐ │ 类型指针 │ ← 存着原始类型Dog / Cat / ... │ 值指针 │ ← 存着原始值 └─────────────┘vars Speaker// nil 接口类型nil值nilsDog{Name:旺财}// 类型Dog值旺财所以接口值判 nil 要注意vars Speaker fmt.Println(snil)// truesDog{Name:旺财}fmt.Println(snil)// false// 注意接口里存了 *Dog(nil) 时接口本身 ! nilvarp*Dognilsp fmt.Println(snil)// false因为接口的类型不是 nil九、实战支付接口用一个贴近业务的例子来收尾。定义接口// Order 代表一笔订单typeOrderstruct{IDstringAmountfloat64Currencystring}// Payer 支付接口——任何支付方式都需要实现 Pay 方法typePayerinterface{Pay(order Order)(string,error)}微信支付实现typeWeChatPaystruct{AppIDstring}func(w WeChatPay)Pay(order Order)(string,error){// 调用微信支付 APIfmt.Printf([微信支付] 订单 %s金额 %.2f %s\n,order.ID,order.Amount,order.Currency)return微信交易号: wx_order.ID,nil}支付宝实现typeAliPaystruct{AppIDstring}func(a AliPay)Pay(order Order)(string,error){// 调用支付宝 APIfmt.Printf([支付宝] 订单 %s金额 %.2f %s\n,order.ID,order.Amount,order.Currency)return支付宝交易号: ali_order.ID,nil}信用卡支付实现typeCreditCardstruct{CardNumberstring}func(c CreditCard)Pay(order Order)(string,error){fmt.Printf([信用卡] 订单 %s金额 %.2f %s\n,order.ID,order.Amount,order.Currency)return信用卡交易号: cc_order.ID,nil}业务代码只面对接口// checkout 只依赖 Payer 接口// 传微信、支付宝、信用卡都行funccheckout(payer Payer,order Order){receipt,err:payer.Pay(order)iferr!nil{fmt.Println(❌ 支付失败:,err)return}fmt.Printf(✅ 支付成功凭证: %s\n,receipt)}funcmain(){order:Order{ID:20240722001,Amount:99.9,Currency:CNY}// 同一个 checkout 函数三种支付方式checkout(WeChatPay{AppID:wx_xxx},order)checkout(AliPay{AppID:ali_xxx},order)checkout(CreditCard{CardNumber:6222****1234},order)}输出[微信支付] 订单 20240722001金额 99.90 CNY ✅ 支付成功凭证: 微信交易号: wx_20240722001 [支付宝] 订单 20240722001金额 99.90 CNY ✅ 支付成功凭证: 支付宝交易号: ali_20240722001 [信用卡] 订单 20240722001金额 99.90 CNY ✅ 支付成功凭证: 信用卡交易号: cc_20240722001为什么这样设计好场景需要做的事新增一种支付方式写一个新 struct实现Pay方法即可切换支付方式调用时换一个实现类就行写单元测试可以写一个MockPayer不用真的扣钱业务代码完全不用改这就是面向接口编程的核心价值——对扩展开放对修改关闭开闭原则。十、总结知识点一句话接口定义一组方法签名隐式实现有方法就自动实现不需要写 implements类型断言s.(Dog)从接口取出原始类型类型 switchswitch v : s.(type)按类型分支处理空接口 anyany 任何类型但不要滥用接口嵌套小接口组合成大接口比继承更灵活面向接口编程业务依赖接口不依赖实现切换实现零成本