傍晚时分,你坐在屋檐下,看着天慢慢地黑下去,心里寂寞而凄凉,感到自己的生命被剥夺了。当时我是个年轻人,但我害怕这样生活下去,衰老下去。在我看来,这是比死亡更可怕的事。——–王小波
写在前面
嗯,学习GO
,所以有了这篇文章
博文内容为《GO语言实战》
读书笔记之一
主要涉及知识
傍晚时分,你坐在屋檐下,看着天慢慢地黑下去,心里寂寞而凄凉,感到自己的生命被剥夺了。当时我是个年轻人,但我害怕这样生活下去,衰老下去。在我看来,这是比死亡更可怕的事。——–王小波
Golang
里面的 多态
是指代码可以根据类型的具体实现采取不同行为的能力。
如果一个类型实现了某个接口,所有使用这个接口的地方,都可以支持这种类型的值
。标准库里有很好的例子,如io
包里实现的流式处理接口。io
包提供了一组构造得非常好的接口和函数,来让代码轻松支持流式数据处理。
只要实现两个接口
,就能利用整个io
包背后的所有强大能力。不过,我们的程序在声明和实现接口时会涉及很多细节。即便实现的是已有接口,也需要了解这些接口是如何工作的。
在探究接口如何工作以及实现的细节之前,我们先来看一下使用标准库里的接口的例子。
标准库 curl 的功能,如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package mainimport ( "fmt" "io" "net/http" "os" ) func init () { if len (os.Args) != 2 { fmt.Println("Usage: ./example2 <url>" ) os.Exit(-1 ) } } func main () { r, err := http.Get(os.Args[1 ]) if err != nil { fmt.Println(err) return } io.Copy(os.Stdout, r.Body) if err := r.Body.Close(); err != nil { fmt.Println(err) } }
http.Response
类型包含一个名为 Body
的字段,这个字段是一个 io.ReadCloser
接口类型的值
io.Copy
函数的第二个参数,接受一个 io.Reader
接口类型的值,这个值表示数据流入的源。Body
字段实现了 io.Reader
接口
io.Copy
的第一个参数是复制到的目标,这个参数必须是一个实现了 io.Writer
接口,os
包里的一个特殊值 Stdout
,表示标准输出设备,已经实现了 io.Writer
接口
如果学过java之类的语言这里横容易理解,类比java中IO读写,低级流包装为高级流进行 IO 处理。
1 2 3 4 5 6 7 8 9 10 11 ┌──[root@liruilongs.github.io]-[/usr/local /go/src] └─$ go run listing34.go Usage: ./example2 <url> exit status 255┌──[root@liruilongs.github.io]-[/usr/local /go/src] └─$ go run listing34.go http://localhost:80 <!DOCTYPE html> <html> <head> <meta charset='utf-8' content="width=device-width, initial-scale=1, maximum-scale=1" name="viewport" > .....
下面的 Demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package mainimport ( "bytes" "fmt" "io" "os" ) func main () { var b bytes.Buffer b.Write([]byte ("Hello" )) fmt.Fprintf(&b, "World!" ) io.Copy(os.Stdout, &b) }
fmt.Fprintf
函数接受一个 io.Writer
类型的接口值作为其第一个参数,bytes.Buffer
类型的指针实现了 io.Writer
接口,bytes.Buffer
类型的指针也实现了 io.Reader
接口,再次使用 io.Copy
函数
1 2 3 4 5 6 7 func Fprintf (w io.Writer, format string , a ...any) (n int , err error) { p := newPrinter() p.doPrintf(format, a) n, err = w.Write(p.buf) p.free() return }
运行代码
1 2 3 ┌──[root@liruilongs.github.io]-[/usr/local /go/src] └─$ go run lsiting35.go HelloWorld!
io.Writer
和 io.Reader
接口
1 2 3 4 5 6 type Writer interface { Write(p []byte ) (n int , err error) } type Reader interface { Read(p []byte ) (n int , err error) }
bytes.Buffer
中上面对应接口的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 func (b *Buffer) Write (p []byte ) (n int , err error) { b.lastRead = opInvalid m, ok := b.tryGrowByReslice(len (p)) if !ok { m = b.grow(len (p)) } return copy (b.buf[m:], p), nil } .... func (b *Buffer) Read (p []byte ) (n int , err error) { b.lastRead = opInvalid if b.empty() { b.Reset() if len (p) == 0 { return 0 , nil } return 0 , io.EOF } n = copy (p, b.buf[b.off:]) b.off += n if n > 0 { b.lastRead = opRead } return n, nil }
实现 接口
是用来定义行为的类型
。这些被定义的行为
不由接口直接实现
,而是通过方法由用户定义的类型实现。也就是我们常讲的实现类
GO
中的类称为 实体类型
,原因是如果离开内部存储的用户定义的类型的实现,接口并没有具体的行为。
并不是所有值都完全等同,用户定义的类型
的值或者指针要满足接口的实现
,需要遵守一些规则
。
展示了在user
类型值赋值后接口变量的值的内部布局。接口值是一个两个字长度的数据结构
,
第一个字包含一个指向内部表的指针。这个内部表叫作iTable,包含了所存储的值的类型信息。
iTable包含了已存储的值的类型信息以及与这个值相关联的一组方法。
第二个字是一个指向所存储值的指针。将类型信息和指针组合在一起,就将这两个值组成了一种特殊的关系
一个指针赋值给接口之后发生的变化。在这种情况里,类型信息会 存储一个指向保存的类型的指针
,而接口值第二个字依旧保存指向实体值的指针
方法集 方法集定义了接口的接受规则。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package mainimport ( "fmt" ) type notifier interface { notify() } type user struct { name string email string } func (u *user) notify () { fmt.Printf("Sending user email to %s<%s>\n" , u.name, u.email) } func main () { u := user{"Bill" , "bill@email.com" } sendNotification(u) func sendNotification (n notifier) { n.notify() }
程序虽然看起来没问题,但实际上却无法通过编译
1 2 3 4 5 6 7 8 9 10 ============================================= GOROOT=C:\Program Files\Go GOPATH=C:\Users\liruilong\go "C:\Program Files\Go\bin\go.exe" build -o C:\Users\liruilong\AppData\Local\JetBrains\GoLand2023.2\tmp\GoLand\___go_build_listing36_go.exe C:\Users\liruilong\Documents\GitHub\golang_code\chapter5\listing36\listing36.go .\listing36.go:32:19: cannot use u (variable of type user) as notifier value in argument to sendNotification: user does not implement notifier (method notify has pointer receiver) Compilation finished with exit code 1
根据提示信息我们可以看到:
不能将 u(类型是 user)作为 sendNotification 的参数类型 notifier:user
类型并没有实现 notifier(notify 方法使用指针接收者声明)
要了解用指针接收者来实现接口时为什么 user
类型的值无法实现该接口,需要先了解方法集
。
方法集定义了一组关联到给定类型的值或者指针的方法
。定义方法时使用的接收者
的类型决定了这个方法是关联到值,还是关联到指针,还是两个都关联
规范里描述的方法集
T --> (t T)
*T --> (t T) and (t *T)
反过来从接收者类型的角度来看方法集:
如果使用指针接收者
来实现一个接口
,那么只有指向那个类型的指针
才能够实现对应的接口
。
1 2 3 4 5 6 7 8 9 type notifier interface { notify() } func (u *user) notify () { fmt.Printf("Sending user email to %s<%s>\n" , u.name, u.email) }
必须使用指针的方式
1 2 3 4 5 6 func main () { u := user{"Bill" , "bill@email.com" } sendNotification(&u) } func sendNotification (n notifier) { n.notify()
如果使用值接收者
来实现一个接口,那么那个类型的值和指针
都能够实现对应的接口
1 2 3 4 5 6 7 8 9 type notifier interface { notify() } func (u user) notify () { fmt.Printf("Sending user email to %s<%s>\n" , u.name, u.email) }
即下面两种方式都可以的
值调用
1 2 3 4 5 6 7 func main () { u := user{"Bill" , "bill@email.com" } sendNotification(u) } func sendNotification (n notifier) { n.notify() }
指针调用
1 2 3 4 5 6 7 func main () { u := user{"Bill" , "bill@email.com" } sendNotification(&u) } func sendNotification (n notifier) { n.notify() }
现在的问题是,为什么会有这种限制?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package mainimport "fmt" type duration int func (d *duration) pretty () string { return fmt.Sprintf("Duration: %d" , *d) } func main () { duration(42 ).pretty() }
上面的代码无法通过编译,duration(42)
,返回的是一个值,并不是一个地址,所以值的方法集只包含使用值的接收者的方法,并不会认为
1 2 3 4 .\listing46.go:19:15: cannot call pointer method pretty on duration Compilation finished with exit code 1
事实上,编译器并不是总能自动获得一个值的地址,
因为不是总能获取一个值的地址
,**所以值的方法集
只包括了使用值接收者实现的方法
**。 而 指针的方法集包括了值接收者和指针接收者
多态 在了解了接口和方法集背后的机制,最后来看一个展示接口的多态行为的例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 package mainimport ( "fmt" ) type notifier interface { notify() } type user struct { name string email string } func (u *user) notify () { fmt.Printf("Sending user email to %s<%s>\n" , u.name, u.email) } type admin struct { name string email string } func (a *admin) notify () { fmt.Printf("Sending admin email to %s<%s>\n" , a.name, a.email) } func main () { bill := user{"Bill" , "bill@email.com" } sendNotification(&bill) lisa := admin{"Lisa" , "lisa@email.com" } sendNotification(&lisa) } func sendNotification (n notifier) { n.notify() }
如果熟悉面向对象编程,这部分东西相对来说很好理解,不同的是在调用的时候,指针接收和值接收需要注意一下。
如果实现方法设置为值接收,那么在调用时,可以使用指针或者值的方式调用,如果实现方法使用指针接收,那么在调用时只能使用指针调用,
即如果使用指针接收者
来实现一个接口
,那么只有指向那个类型的指针
才能够实现对应的接口
。如果使用值接收者
来实现一个接口,那么那个类型的值和指针
都能够实现对应的接口
博文部分内容参考 © 文中涉及参考链接内容版权归原作者所有,如有侵权请告知
《GO语言实战》
© 2018-至今 liruilonger@gmail.com , All rights reserved. 保持署名-非商用-相同方式共享(CC BY-NC-SA 4.0)