Golang 基础:接口使用实现原理(eface iface)和设计模式

Posted 拭心

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Golang 基础:接口使用实现原理(eface iface)和设计模式相关的知识,希望对你有一定的参考价值。

文章目录

本文是我学习 Go TourGo 语言第一课 接口相关章节的笔记,如有理解不当之处,恳请留言指出,感谢!

定义接口

  • 接口里的方法,参数要么都有名字,要么都没有,否则报错: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):

  1. eface 用于表示没有方法的空接口(empty interface)类型变量,也就是 interface类型的变量;
  2. 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)和设计模式

Golang 接口定义实现(eface iface)和设计模式

[golang]语法基础之接口

使用golang实现令牌桶限流和时间窗口控制

使用golang实现令牌桶限流和时间窗口控制

golang基础--Interface接口