前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Golang 基础:接口使用、实现原理(eface iface)和设计模式

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

作者头像
张拭心 shixinzhang
发布2022-05-10 08:12:25
5310
发布2022-05-10 08:12:25
举报

文章目录

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

定义接口

  • 接口里的方法,参数要么都有名字,要么都没有,否则报错:Method specification has both named and unnamed parameters
  • 同时,方法名称不能重复,哪怕参数不一样也不可以,否则会报错:Duplicate method 'XXX
代码语言:javascript
复制
type People interface {
	M1(int) int;
	M2(string);
}

type KnowledgeMan interface {
	M3(string);
}

type StudentRepo interface {
	//嵌入
	People
	KnowledgeMan
}

一个接口可以嵌入其他接口,但要求方法如果重名必须参数一致。

实现接口

代码语言:javascript
复制
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{} 的别名。

代码语言:javascript
复制
//空类型做参数,参数可以传递任意类型
func TestEmptyInterface(i interface{})  {
	fmt.Println(i)
}

func main() {
	//interface{} 是空接口类型,任意类型都认为实现了空接口
	var i interface{} = 15
	fmt.Println(i)

	//参数类型使用空接口的话,可以当作泛型使用
	TestEmptyInterface(111)
	TestEmptyInterface("shixin")
}

上面的代码中,先定义了空接口类型的 i,同时赋值为 15,之所以可以这样,是因为按照前面接口实现的定义“定义了相同签名方法就算实现了接口”的逻辑,空接口没有方法,那所有类型都可以说实现了空接口。

空接口的这种特性,可以用作泛型,比如作为方法参数等场景,这样可以传递不同类型的参数。

类型断言

类型断言:判断变量是否为某种接口的实现。

代码语言:javascript
复制
v, ok := i.(T)

i.(T) 的意思是判断变量 i 是否为 T 的类型。

这要求 i 的类型必须是接口,否则会报错: Invalid type assertion: intValue.(int64) (non-interface type int64 on left)

举个例子:

代码语言:javascript
复制
	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 会赋值为异常值,但类型会赋值为用于判断的类型。

运行结果:

代码语言:javascript
复制
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 类型变量。
代码语言:javascript
复制
// $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

代码语言:javascript
复制
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 可以打印出接口的类型和数据信息

输出:

代码语言:javascript
复制
(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)”

代码语言:javascript
复制
/ $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.包装器模式:参数与返回值一样,在函数内部做数据过滤、变换等操作

可以将多个接受同一接口类型参数的包装函数组合成一条链来调用:

代码语言:javascript
复制
// $GOROOT/src/io/io.go
func LimitReader(r Reader, n int64) Reader { return &LimitedReader{r, 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 &capitalizedReader{r: 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.适配器模式:将函数,转换成特定类型,成为某个接口的实现

代码语言:javascript
复制
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 类型:

代码语言:javascript
复制
// $GOROOT/src/net/http/server.go
func ListenAndServe(addr string, handler Handler) error {
	server := &Server{Addr: 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 类型实例

代码语言:javascript
复制
func main() { 
	http.ListenAndServe(":8080", logHandler(authHandler(http.HandlerFunc(greetings))))
}

7.尽量不要使用空接口类型,编译器无法做类型检查,安全没有保证。

使用interface{}作为参数类型的函数或方法都有一个共同特点,就是它们面对的都是未知类型的数据,所以在这里使用具有“泛型”能力的interface{}类型 等 Go 泛型落地后,很多场合下 interface{}就可以被泛型替代了。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-04-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 文章目录
  • 定义接口
  • 实现接口
  • 空接口
  • 类型断言
  • 接口类型在运行时是如何实现的 🔥
  • 接口设计的 7 个建议 🔥
相关产品与服务
消息队列 TDMQ
消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档