Golang 基础:接口使用实现原理(eface iface)和设计模式
Posted 拭心
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Golang 基础:接口使用实现原理(eface iface)和设计模式相关的知识,希望对你有一定的参考价值。
文章目录
本文是我学习 Go Tour 和 Go 语言第一课 接口相关章节的笔记,如有理解不当之处,恳请留言指出,感谢!
定义接口
- 接口里的方法,参数要么都有名字,要么都没有,否则报错:
Method specification has both named and unnamed parameters
- 同时,方法名称不能重复,哪怕参数不一样也不可以,否则会报错:
Duplicate method 'XXX
。
type People interface
M1(int) int;
M2(string);
type KnowledgeMan interface
M3(string);
type StudentRepo interface
//嵌入
People
KnowledgeMan
一个接口可以嵌入其他接口,但要求方法如果重名必须参数一致。
实现接口
type KnowledgeMan interface
M3(string);
type Impl struct
//只要包含相同签名的方法,就算是实现了接口
func (i *Impl)M3(s string)
fmt.Println(s)
func main()
//&Impl: new 一个 Impl
var student KnowledgeMan = &Impl
student.M3("haha")
如上代码所示,只要一个类型中定义了接口的所有方法(相同签名),就算是实现了接口,就可以赋值给这个接口类型的变量。
空接口
空接口:interface
空接口的这个抽象对应的事物集合空间包含了 Go 语言世界的所有事物。
go1.18 增加了 any 关键字,用以替代现在的 interface 空接口类型:type any = interface,实际上是 interface 的别名。
//空类型做参数,参数可以传递任意类型
func TestEmptyInterface(i interface)
fmt.Println(i)
func main()
//interface 是空接口类型,任意类型都认为实现了空接口
var i interface = 15
fmt.Println(i)
//参数类型使用空接口的话,可以当作泛型使用
TestEmptyInterface(111)
TestEmptyInterface("shixin")
上面的代码中,先定义了空接口类型的 i,同时赋值为 15,之所以可以这样,是因为按照前面接口实现的定义“定义了相同签名方法就算实现了接口”的逻辑,空接口没有方法,那所有类型都可以说实现了空接口。
空接口的这种特性,可以用作泛型,比如作为方法参数等场景,这样可以传递不同类型的参数。
类型断言
类型断言:判断变量是否为某种接口的实现。
v, ok := i.(T)
i.(T)
的意思是判断变量 i 是否为 T 的类型。
这要求 i 的类型必须是接口,否则会报错:
Invalid type assertion: intValue.(int64) (non-interface type int64 on left)
举个例子:
var intValue int64 = 123
var anyType interface = intValue
//类型匹配,v 是值,ok 是 boolean
v,ok := anyType.(int64)
fmt.Printf("value:%d, ok:%t, type of v: %T\\n", v, ok, v)
//如果不是这个类型,v2
v2, ok := anyType.(string)
fmt.Printf("v2 value:%d, ok:%t, type of v: %T\\n", v2, ok, v2)
v3 := anyType.(int64)
fmt.Printf("v3 value:%d, type of v: %T\\n", v3, v3)
//类型不对,会直接 panic 报错
v4 := anyType.([]int)
fmt.Printf("v4 value:%d, type of v: %T\\n", v4, v4)
上面的代码中,定义了一个空接口,赋值为一个 int64 类型的值。然后我们判断类型是否为 int64,输出结果符合预期。
用一个其他类型判断的时候,v 会赋值为异常值,但类型会赋值为用于判断的类型。
运行结果:
value:123, ok:true, type of v: int64
v2 value:%!d(string=), ok:false, type of v: string
v3 value:123, type of v: int64
panic: interface conversion: interface is int64, not []int
goroutine 1 [running]:
main.TestInterface()
/Users/simon/go/src/awesomeProject/main.go:258 +0x491
main.main()
/Users/simon/go/src/awesomeProject/main.go:278 +0x25
exit status 2
开发建议
- 接口越大,抽象程度越弱。建议接口越小越好,职责单一(一般建议接口方法数量在 3 个以内)
- 先抽象,然后再优化为小接口,循序渐进
越偏向业务层,抽象难度就越高,尽量在业务以下多抽象分离
接口类型在运行时是如何实现的 🔥
https://time.geekbang.org/column/article/473414
每个接口类型变量在运行时的表示都是由两部分组成的,类型和数据。
eface(_type, data)和iface(tab, data):
- eface 用于表示没有方法的空接口(empty interface)类型变量,也就是 interface类型的变量;
- iface 用于表示其余拥有方法的接口 interface 类型变量。
// $GOROOT/src/runtime/runtime2.go
type iface struct
tab *itab
data unsafe.Pointer
type eface struct
_type *_type
data unsafe.Pointer
// $GOROOT/src/runtime/runtime2.go
type itab struct
inter *interfacetype
_type *_type
hash uint32 // copy of _type.hash. Used for type switches.
_ [4]byte
fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
// $GOROOT/src/runtime/type.go
type _type struct
size uintptr
ptrdata uintptr // size of memory prefix holding all pointers
hash uint32
tflag tflag
align uint8
fieldAlign uint8
kind uint8
// function for comparing objects of this type
// (ptr to object A, ptr to object B) -> ==?
equal func(unsafe.Pointer, unsafe.Pointer) bool
// gcdata stores the GC type data for the garbage collector.
// If the KindGCProg bit is set in kind, gcdata is a GC program.
// Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
gcdata *byte
str nameOff
ptrToThis typeOff
// $GOROOT/src/runtime/type.go
type interfacetype struct
typ _type
pkgpath name
mhdr []imethod
判断两个接口变量是否相同,需要判断 _type/tab 和 data 指向的内存数据是否相同。
只有两个接口类型变量的类型信息(eface._type/iface.tab._type)相同,且数据指针(eface.data/iface.data)所指数据相同时,两个接口类型变量才是相等的。
未显式初始化的接口类型变量的值为nil,这个变量的 _type/tab 和 data 都为 nil。
空接口或非空类型接口没有赋值,都为 nil
func TestNilInterface()
var i interface
var e error
println(i) //(0x0,0x0) : 类型信息、数据值信息均为空
println(e)
fmt.Println(i) //<nil>
fmt.Println(e)
fmt.Println("i == nil", i == nil)
fmt.Println("e == nil", e == nil)
fmt.Println("i == e", e == i)
println 可以打印出接口的类型和数据信息
输出:
(0x0,0x0)
(0x0,0x0)
<nil>
<nil>
i == nil true
e == nil true
i == e true
接口类型变量的赋值是一种装箱操作
接口类型的装箱实际就是创建一个 eface 或 iface 的过程,需要拷贝内存,成本较大。
接口设计的 7 个建议 🔥
1.类型组合:
- 接口定义中嵌入其他接口,实现功能更多的接口
- 结构体中嵌入接口,等于实现了这个接口
- 结构体中嵌入其他结构体,后面调用嵌入的结构体成员,会被“委派”给嵌入的实例
Go 中没有继承父类功能的概念,而是通过类型嵌入的方式,组合不同类型的功能。
被嵌入的类不知道谁嵌入了它,也无法向上向下转型,所以 Go 中没有“父子类”的继承关系。
2.用接口作为“关节(连接点)”:在函数定义时,参数要多用接口类型。
3.在创建某一类型实例时可以: “接受接口,返回结构体(Accept interfaces, return structs)”
/ $GOROOT/src/log/log.go
type Logger struct
mu sync.Mutex
prefix string
flag int
out io.Writer
buf []byte
func New(out io.Writer, prefix string, flag int) *Logger
return &Logger
out: out,
prefix: prefix,
flag: flag
4.包装器模式:参数与返回值一样,在函数内部做数据过滤、变换等操作
可以将多个接受同一接口类型参数的包装函数组合成一条链来调用:
// $GOROOT/src/io/io.go
func LimitReader(r Reader, n int64) Reader return &LimitedReaderr, n
type LimitedReader struct
R Reader // underlying reader
N int64 // max bytes remaining
func (l *LimitedReader) Read(p []byte) (n int, err error)
// ... ...
func CapReader(r io.Reader) io.Reader
return &capitalizedReaderr: r
type capitalizedReader struct
r io.Reader
func (r *capitalizedReader) Read(p []byte) (int,
error)
n, err := r.r.Read(p)
if err != nil
return 0, err
q := bytes.ToUpper(p)
for i, v := range q
p[i] = v
return n, err
func main()
r := strings.NewReader("hello, gopher!\\n")
r1 := CapReader(io.LimitReader(r, 4)) //链式调用
if _, err := io.Copy(os.Stdout, r1); err != nil
log.Fatal(err)
5.适配器模式:将函数,转换成特定类型,成为某个接口的实现
func greetings(w http.ResponseWriter, r *http.Request)
fmt.Fprintf(w, "Welcome!")
func main()
http.ListenAndServe(":8080", http.HandlerFunc(greetings))
http.HandlerFunc
把 greetings 转成了 http.Handler
类型:
// $GOROOT/src/net/http/server.go
func ListenAndServe(addr string, handler Handler) error
server := &ServerAddr: addr, Handler: handler
return server.ListenAndServe()
type Handler interface
ServeHTTP(ResponseWriter, *Request)
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request)
f(w, r)
通过类型转换,HandlerFunc 让一个普通函数成为实现 ServeHTTP 方法的对象,从而满足http.Handler接口。
6.中间件
中间件就是包装函数,类似责任链模式。
在 Go Web 编程中,“中间件”常常指的是一个实现了 http.Handler 接口的 http.HandlerFunc 类型实例
func main()
http.ListenAndServe(":8080", logHandler(authHandler(http.HandlerFunc(greetings))))
7.尽量不要使用空接口类型,编译器无法做类型检查,安全没有保证。
使用interface作为参数类型的函数或方法都有一个共同特点,就是它们面对的都是未知类型的数据,所以在这里使用具有“泛型”能力的interface类型
等 Go 泛型落地后,很多场合下 interface就可以被泛型替代了。
以上是关于Golang 基础:接口使用实现原理(eface iface)和设计模式的主要内容,如果未能解决你的问题,请参考以下文章
Golang 接口定义实现(eface iface)和设计模式