本文是我学习 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 的类型必须是接口,否则会报错:
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 &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.适配器模式:将函数,转换成特定类型,成为某个接口的实现


func greetings(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Welcome!")
}
func main() {
    http.ListenAndServe(":8080", http.HandlerFunc(greetings))
}
http.HandlerFunchttp.Handler
// $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 类型实例

func main() { 
	http.ListenAndServe(":8080", logHandler(authHandler(http.HandlerFunc(greetings))))
}

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

使用interface{}作为参数类型的函数或方法都有一个共同特点,就是它们面对的都是未知类型的数据,所以在这里使用具有“泛型”能力的interface{}类型

等 Go 泛型落地后,很多场合下 interface{}就可以被泛型替代了。