GO语言实战之接口实现与方法集

news/2024/11/19 19:30:49/

写在前面


  • 嗯,学习GO,所以有了这篇文章
  • 博文内容为《GO语言实战》读书笔记之一
  • 主要涉及知识
    • 接口是什么
    • 方法集(值接收和指针接收)
    • 多态

傍晚时分,你坐在屋檐下,看着天慢慢地黑下去,心里寂寞而凄凉,感到自己的生命被剥夺了。当时我是个年轻人,但我害怕这样生活下去,衰老下去。在我看来,这是比死亡更可怕的事。--------王小波


Golang 里面的 多态 是指代码可以根据类型的具体实现采取不同行为的能力。

如果一个类型实现了某个接口,所有使用这个接口的地方,都可以支持这种类型的值。标准库里有很好的例子,如io包里实现的流式处理接口。io包提供了一组构造得非常好的接口和函数,来让代码轻松支持流式数据处理。

只要实现两个接口,就能利用整个io包背后的所有强大能力。不过,我们的程序在声明和实现接口时会涉及很多细节。即便实现的是已有接口,也需要了解这些接口是如何工作的。

在探究接口如何工作以及实现的细节之前,我们先来看一下使用标准库里的接口的例子。

标准库

curl 的功能,如

//  这个示例程序展示如何使用 io.Reader 和 io.Writer 接口
//  写一个简单版本的 curl 程序
package mainimport ("fmt""io""net/http""os"
)// init 在 main 函数之前调用
func init() {if len(os.Args) != 2 {fmt.Println("Usage: ./example2 <url>")os.Exit(-1)}
}// main 是应用程序的入口
func main() {// 从 Web 服务器得到响应r, err := http.Get(os.Args[1])if err != nil {fmt.Println(err)return}// 从 Body 复制到 Stdoutio.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 处理。

┌──[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

// Sample program to show how a bytes.Buffer can also be used
// 用于 io.Copy 函数
package mainimport ("bytes""fmt""io""os"
)// main is the entry point for the application.
func main() {var b bytes.Buffer// 将字符串写入 Bufferb.Write([]byte("Hello"))//  使用 Fprintf 将字符串拼接到 Bufferfmt.Fprintf(&b, "World!")// 将 Buffer 的内容写到 Stdoutio.Copy(os.Stdout, &b)
}

fmt.Fprintf 函数接受一个 io.Writer 类型的接口值作为其第一个参数,bytes.Buffer 类型的指针实现了 io.Writer 接口,bytes.Buffer 类型的指针也实现了 io.Reader 接口,再次使用 io.Copy 函数

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
}

运行代码

┌──[root@liruilongs.github.io]-[/usr/local/go/src]
└─$ go run lsiting35.go
HelloWorld!

io.Writerio.Reader 接口

type Writer interface {Write(p []byte) (n int, err error)
}
type Reader interface {Read(p []byte) (n int, err error)
}

bytes.Buffer 中上面对应接口的实现

func (b *Buffer) Write(p []byte) (n int, err error) {b.lastRead = opInvalidm, 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 = opInvalidif b.empty() {// Buffer is empty, reset to recover space.b.Reset()if len(p) == 0 {return 0, nil}return 0, io.EOF}n = copy(p, b.buf[b.off:])b.off += nif n > 0 {b.lastRead = opRead}return n, nil
}

实现

接口是用来定义行为的类型。这些被定义的行为不由接口直接实现,而是通过方法由用户定义的类型实现。也就是我们常讲的实现类

GO 中的类称为 实体类型,原因是如果离开内部存储的用户定义的类型的实现,接口并没有具体的行为。不像 Java 中有默认方法之类的操作。

并不是所有值都完全等同,用户定义的类型的值或者指针要满足接口的实现,需要遵守一些规则

展示了在user类型值赋值后接口变量的值的内部布局。接口值是一个两个字长度的数据结构,

  • 第一个字包含一个指向内部表的指针。这个内部表叫作iTable,包含了所存储的值的类型信息。iTable包含了已存储的值的类型信息以及与这个值相关联的一组方法。

  • 第二个字是一个指向所存储值的指针。将类型信息和指针组合在一起,就将这两个值组成了一种特殊的关系

在这里插入图片描述

一个指针赋值给接口之后发生的变化。在这种情况里,类型信息会 存储一个指向保存的类型的指针,而接口值第二个字依旧保存指向实体值的指针

在这里插入图片描述

方法集

方法集定义了接口的接受规则。

// 这个示例程序展示 Go 语言里如何使用接口
package mainimport ("fmt"
)// notifier 是一个定义了
// 通知类行为的接口
type notifier interface {notify()
}// user 在程序里定义一个用户类型
type user struct {name  stringemail string
}//  notify 是使用指针接收者实现的方法
func (u *user) notify() {fmt.Printf("Sending user email to %s<%s>\n",u.name,u.email)
}// main is the entry point for the application.
func main() {// Create a value of type User and send a notification.u := user{"Bill", "bill@email.com"}sendNotification(u)func sendNotification(n notifier) {n.notify()
}

程序虽然看起来没问题,但实际上却无法通过编译

在这里插入图片描述

=============================================GOROOT=C:\Program Files\Go #gosetup
GOPATH=C:\Users\liruilong\go #gosetup
"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 #gosetup
# command-line-arguments
.\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)

反过来从接收者类型的角度来看方法集:

如果使用指针接收者来实现一个接口,那么只有指向那个类型的指针才能够实现对应的接口

  • (t T) --> T and *T
type notifier interface {notify()
}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()

如果使用值接收者来实现一个接口,那么那个类型的值和指针都能够实现对应的接口

  • (t *T) --> *T
type notifier interface {notify()
}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()
}

指针调用

func main() {u := user{"Bill", "bill@email.com"}sendNotification(&u)
}
func sendNotification(n notifier) {n.notify()
}

现在的问题是,为什么会有这种限制?

package mainimport "fmt"// duration 是一个基于 int 类型的类型
type duration int// 使用更可读的方式格式化 duration 值
func (d *duration) pretty() string {return fmt.Sprintf("Duration: %d", *d)
}// main 是应用程序的入口
func main() {//d := duration(42)//d.pretty()duration(42).pretty()// ./listing46.go:17:不能通过指针调用 duration(42)的方法// ./listing46.go:17: 不能获取 duration(42)的地址
}

上面的代码无法通过编译,duration(42) ,返回的是一个值,并不是一个地址,所以值的方法集只包含使用值的接收者的方法。

# command-line-arguments
.\listing46.go:19:15: cannot call pointer method pretty on durationCompilation finished with exit code 1

事实上,编译器并不是总能自动获得一个值的地址, 因为不是总能获取一个值的地址所以值的方法集只包括了使用值接收者实现的方法 。 而 指针的方法集包括了值接收者和指针接收者

多态

在了解了接口和方法集背后的机制,最后来看一个展示接口的多态行为的例子

// Sample program to show how polymorphic behavior with interfaces.
package mainimport ("fmt"
)type notifier interface {notify()
}
type user struct {name  stringemail string
}func (u *user) notify() {fmt.Printf("Sending user email to %s<%s>\n",u.name,u.email)
}type admin struct {name  stringemail 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-2023 liruilonger@gmail.com, All rights reserved. 保持署名-非商用-相同方式共享(CC BY-NC-SA 4.0)


http://www.ppmy.cn/news/1103705.html

相关文章

Spring Boot整合Spring Data Jpa + QueryDSL

简介 Spring Data JPA是一个Spring项目中常用的持久化框架&#xff0c;它简化了与数据库进行交互的过程。而QueryDSL是一个查询构建框架&#xff0c;可以让我们以面向对象的方式来编写数据库查询。 在本文中&#xff0c;我们将讨论如何使用Spring Boot整合Spring Data JPA和Q…

深度学习常用的Python库(核心库、可视化、NLP、计算机视觉、深度学习等)

&#xff08;1&#xff09;核心库与统计&#xff1a;Numpy、Scipy、Pandas、StatsModels。 &#xff08;2&#xff09;可视化&#xff1a;Matplotlib、Seaborn、Plotly、Bokeh、Pydot、Scikit-learn、XGBoost/LightGBM/CatBoost、Eli5。 &#xff08;3&#xff09;深度学习&a…

第28节-PhotoShop基础课程-图层操作

文章目录 前言1.像素图层2.删除 Delete3.合并 Ctrl E4.盖印 Ctrl Shift Alt5.图层顺序-拖动就可以6.编组-Ctrl G 管理图层-分类存放7.锁定图层-背景图层8.不透明度9.查找图层 2.智能图层1.能保持图片放大缩小&#xff08;Ctrl T&#xff09;的时候不丢失分辨率2.和滤镜配合使…

【IMX6ULL驱动开发学习】24.关于mmap为什么能直接操作LCD显示

记录今天面试中遇到的一个提问&#xff0c;当时没有答上来 感谢面试官&#xff08;弓总&#xff09;的提问&#xff0c;让我认识到了目前的不足&#xff0c;下午又深入的学习了一下&#xff0c;在这里做一下补充 mmap为什么能直接操作LCD显示 首先在内核空间申请一段或多段内存…

常见面试题记录

记录下java的常见面试题 文章目录 记录如下 记录如下 记录如下 hashmap原理lock原理synchronized锁优化过程线程状态以及创建方式线程池&#xff08;执行过程&#xff0c;参数&#xff0c;淘汰策略&#xff09;jvm&#xff08;gc优化和OOM&#xff09;volatile&#xff08;可见…

无损压缩算法

无损压缩算法是一种压缩数据的方法&#xff0c;可以在不丢失任何信息的情况下减小文件的大小。这种算法通常通过消除冗余或者利用统计特性来实现压缩效果。 以下是几种常见的无损压缩算法&#xff1a; 哈夫曼编码&#xff1a;哈夫曼编码是一种基于字符出现频率的压缩算法。它通…

详细介绍mysql表格id清零的方法

文章目录 方法一&#xff1a;利用TRUNCATE TABLE语句清空表格并重置id方法二&#xff1a;利用ALTER TABLE语句修改自增长id的初始值方法三&#xff1a;利用DELETE语句删除表格中的数据并重置id总结 MySQL是一种关系型数据库管理系统&#xff0c;被广泛应用于各种应用程序中。在…

python强制停止线程学习

参考&#xff1a; Python进阶之路 - Timeout | 超时中断 - 知乎 (zhihu.com) 写的很棒。 这里只记录我摘取的封装的一个class: #!/usr/bin/env python # -*- coding: utf-8 -*-import ctypes import threadingclass ThreadKillOver(RuntimeError):"""线程杀…