傍晚时分,你坐在屋檐下,看着天慢慢地黑下去,心里寂寞而凄凉,感到自己的生命被剥夺了。当时我是个年轻人,但我害怕这样生活下去,衰老下去。在我看来,这是比死亡更可怕的事。——–王小波 
 
写在前面  
嗯,学习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)