本文是我学习 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):
- 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 &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{}就可以被泛型替代了。