Golang中正确使用接口的方式?
Golang中接口的作用:
1. 可以作为函数和方法的参数或者返回值的使用,可以通过类型断言和switch方法
2. 多态的使用,在程序设计中,抽象出某些对象共同拥有的方法,多种类型实现同一接口,通过接口变量指向具体对象操作这些方法。
Golang接口的使用
interface 是方法或行为声明的集合,其中所有的方法都没有方法体,接口中也不能有其他变量
interface接口方式实现比较隐性,任何类型的对象必须实现interface所包含的全部方法,则表明该类型实现了该接口。
interface还可以作为一中通用的类型,其他类型变量可以给interface声明的变量赋值。
interface 可以作为一种数据类型,实现了该接口的任何对象都可以给对应的接口类型变量赋值。
一个自定义的类型可以实现多个接口,可以理解为多继承可以使用多个方法,一个接口(比如A接口)可以继承多个别的接口(比如B,C接口),这时如果要实现A接口,也必须将B,C接口的方法也全部实现
具体使用
一个接口本身不能创建实例,但是可以指向一个实现该接口的自定义类型的实例,自定义类型既可以是函数也可以是结构体,如果自定义的类型没有实现接口,就不可以将结构体赋给接口
自定义类型为结构体
package main import ( "fmt" ) // 结构体类型 type Struct struct { } // 实现Invoker的Call func (s *Struct) Call(p interface{}) { fmt.Println("from struct", p) } // 调用器接口 type Invoker interface { // 需要实现一个Call()方法 Call(interface{}) } func main() { // 声明接口变量 var invoker Invoker // 实例化结构体 // s := new(Struct) var s Struct // 将实例化的结构体赋值到接口 invoker = &s // 使用接口调用实例化结构体的方法Struct.Call invoker.Call("hello") }
自定义类型为函数
package main import ( "fmt" ) // 函数定义为类型 type FuncCaller func(interface{}) // 实现Invoker的Call func (f FuncCaller) Call(p interface{}) { // 调用f()函数本体 f(p) } // 调用器接口 type Invoker interface { // 需要实现一个Call()方法 Call(interface{}) } func main() { // 声明接口变量 var invoker Invoker // 将匿名函数转为FuncCaller类型, 再赋值给接口 invoker = FuncCaller(func(v interface{}) { fmt.Println("from function", v) }) // 使用接口调用FuncCaller.Call, 内部会调用函数本体 invoker.Call("hello") }
指向接口的指针
如果希望接口方法修改基础数据,则必须使用指针传递(将对象指针赋值给接口变量)。
type Father interface { f() } type Son1 struct { Name string } func (s Son1) f() { s.Name = "C" } type Son2 struct { Name string } func (s *Son2) f() { s.Name = "C" } func main() { var son1 Father = Son1{Name: "A"} fmt.Println("son1 before name:", son1) son1.f() fmt.Println("son1 after name:", son1) var son2 Father = &Son2{Name: "B"} fmt.Println("son2 before name:", son2) son2.f() fmt.Println("son2 after name:", son2) } console: son1 before name: {A} son1 after name: {A} son2 before name: &{B} son2 after name: &{C} //传递指针才修改成功
接口的合理性验证
- 将实现特定接口的导出类型作为接口API 的一部分进行检查
- 实现同一接口的(导出和非导出)类型属于实现类型的集合
- 任何违反接口合理性检查的场景,都会终止编译, 并通知给用户
错误使用接口会在编译期报错. 所以可以利用这个机制让部分问题在编译期暴露。在实际的项目中可能会导致代码冗余。
type temp interface { f() } type Father struct { } // 实现 f() 方法 func (father Father) f() { fmt.Println("father transfer f()") } //可以理解为继承 type Son struct {
temp //Father Name string } //不实现就直接调用用Father的方法 //func (son Son) f() { // fmt.Println("son transfer f()") //} func main() { //如果没有把Father作为匿名参数,编译期不会报错,运行时会报错 var d temp = Son{} d.f() }
这段代码在编译时不会报错,但是运行时会报错(var _ temp = Son{}可以帮助我们在编译时发现错误),Son没有实现f方法,如果去掉Father的注释可以正常运行,而且可以调用f方法
通过匿名接口的方式对接口方法重写
type temp interface { f() } type Father struct { } // 实现 eat() 方法 func (father Father) f() { fmt.Println("father transfer f()") } //直接将接口作为匿名参数传入 type Son struct { temp Name string } // 实现 eat() 方法 func (son Son) f() { fmt.Println("son transfer f()") } func main() { //如果想直接使用其他实现该接口的类型,需要作为参数传入 father := &Father{} var d temp = &Son{father,"lee"} d.f() }
这种方式与上面的【实现接口的类型】作为匿名参数是一样的效果
在实际使用中,还需根据继承的具体类型判断,赋值的右边应该是断言类型的零值。对于指针类型(如*Handler
)、切片和映射,这是nil
;对于结构类型,这是空结构。
Good代码
type Handler struct { // ... } var _ http.Handler = (*Handler)(nil)
type LogHandler struct {
h http.Handler
log *zap.Logger
}
var _ http.Handler = LogHandler{}
接收器 (receiver) 与接口,接口与具体方法集的匹配
一个类型可以有值接收器方法集和指针接收器方法集
使用值接收器的方法既可以通过值调用,也可以通过指针调用。带指针接收器的方法只能通过指针或 addressable values调用(其实和值调用类似)。
如果方法的接收者是值类型,无论调用者是对象还是对象指针,修改的都是对象的副本,不影响调用者;如果方法的接收者是指针类型,则调用者修改的是指针指向的对象本身。
通常我们使用指针作为方法的接收者的理由:
- 使用指针方法能够修改接收者指向的值。
- 可以避免在每次调用方法时复制该值,在值的类型为大型结构体时,这样做会更加高效。
type S struct { data string } func (s S) Read() string { return s.data } func (s *S) Write(str string) { s.data = str } sVals := map[int]S{1: {"A"}} // 你只能通过值调用 Read sVals[1].Read() // 这不能编译通过: // sVals[1].Write("test") sPtrs := map[int]*S{1: {"A"}} // 通过指针既可以调用 Read,也可以调用 Write 方法 sPtrs[1].Read() sPtrs[1].Write("test")
map中的值是无法取地址的,因为它不是一个变量。这里的值对象仅仅指一些拿不到地址的情况。
sVals := S{"A"} sVals.Write("test")
这种情况编译是可以通过的,这就是通过addressable values调用的
接口的匹配
- 值方法集和接口匹配
- 给接口变量赋值的不管是值还是指针对象,都ok,因为都包含值方法集
- 指针方法集和接口匹配
- 只能将指针对象赋值给接口变量,因为只有指针方法集和接口匹配
- 如果将值对象赋值给接口变量,会在编译期报错(会触发接口合理性检查机制)
type F interface { f() } type S1 struct{} func (s S1) f() {} type S2 struct{} func (s *S2) f() {} s1Val := S1{} s1Ptr := &S1{} s2Val := S2{} s2Ptr := &S2{} var i F i = s1Val i = s1Ptr i = s2Ptr // 下面代码无法通过编译。因为 s2Val 是一个值,而 S2 的 f 方法中没有使用值接收器,值方法集与接口不匹配 // i = s2Val
关于interface关键字
interface有两种表现形式,空接口eface、非空接口iface
其底层都是由不同的struct表示
eface底层结构
type eface struct { // 16 字节 _type *_type //类型信息 data unsafe.Pointer //指向数据的指针 }
它就是代码中经常用到的interface{},
Case 1 :
interface{}内部除了指针还有类型,当做图中的赋值,它还携带着类型就不再是nil了
Case 2:
因为interface{}本身就可以接收任意类型的参数,而*interface{}只能接收*interface{}的参数
iface就是带有方法声明的接口,其底层结构
type iface struct { // 16 字节 tab *itab // 这里面包含了接口自身信息,具体类型信息等 data unsafe.Pointer }
Case 1:
在赋值的时候就以及带着结构体的各种类型信息了
本文来自博客园,作者:LeeJuly,转载请注明原文链接:https://www.cnblogs.com/peterleee/p/13886270.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义