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

news2024/9/22 23:27:55

写在前面


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

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


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

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

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

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

标准库

curl 的功能,如

//  这个示例程序展示如何使用 io.Reader 和 io.Writer 接口
//  写一个简单版本的 curl 程序
package main

import (
 "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 复制到 Stdout
 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 处理。

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

import (
 "bytes"
 "fmt"
 "io"
 "os"
)

// main is the entry point for the application.
func main() {
 var b bytes.Buffer

 // 将字符串写入 Buffer
 b.Write([]byte("Hello"))

 //  使用 Fprintf 将字符串拼接到 Buffer
 fmt.Fprintf(&b, "World!")

 // 将 Buffer 的内容写到 Stdout
 io.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 = 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() {
  // 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 += n
 if n > 0 {
  b.lastRead = opRead
 }
 return n, nil
}

实现

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

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

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

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

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

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

在这里插入图片描述

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

在这里插入图片描述

方法集

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

// 这个示例程序展示 Go 语言里如何使用接口
package main

import (
 "fmt"
)

// notifier 是一个定义了
// 通知类行为的接口
type notifier interface {
 notify()
}

// user 在程序里定义一个用户类型
type user struct {
 name  string
 email 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 main

import "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 duration

Compilation finished with exit code 1

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

多态

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

// Sample program to show how polymorphic behavior with interfaces.
package main

import (
 "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-2023 liruilonger@gmail.com, All rights reserved. 保持署名-非商用-相同方式共享(CC BY-NC-SA 4.0)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/986694.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

FastDFS介绍

文章目录 一、什么是FastDFS二、FastDFS的架构2.1 跟踪服务器&#xff08;tracker server&#xff09;2.2 存储服务器&#xff08;storage server&#xff09;2.3 客户端&#xff08;client&#xff09; 三、 FastDFS功能逻辑分析3.1 upload file&#xff08;上传文件&#xff…

shell入门运算符操作、条件判断

♥️作者&#xff1a;小刘在C站 ♥️个人主页&#xff1a; 小刘主页 ♥️努力不一定有回报&#xff0c;但一定会有收获加油&#xff01;一起努力&#xff0c;共赴美好人生&#xff01; ♥️学习两年总结出的运维经验&#xff0c;以及思科模拟器全套网络实验教程。专栏&#xf…

分类算法系列⑤:决策树

目录 1、认识决策树 2、决策树的概念 3、决策树分类原理 基本原理 数学公式 4、信息熵的作用 5、决策树的划分依据之一&#xff1a;信息增益 5.1、定义与公式 5.2、⭐手动计算案例 5.3、log值逼近 6、决策树的三种算法实现 7、API 8、⭐两个代码案例 8.1、决策树…

springboot使用切面记录接口访问日志

前言 当我们开发和维护一个复杂的应用程序时&#xff0c;了解应用程序的运行情况变得至关重要。特别是在生产环境中&#xff0c;我们需要追踪应用程序的各个方面&#xff0c;以确保它正常运行并能够及时发现潜在的问题。其中之一关键的方面是记录应用程序的接口访问日志。 Sp…

Linux系统离线安装RabbitMQ

安装rabbitmq 1、下载安装包 首先进入官网进行安装包的下载&#xff0c;在下载时一定要注意erlong版本和rabbitmq-server版本匹配 rabbitmq版本对应关系&#xff1a;传送门 Erlong下载地址:传送门 rabbitmq-server下载地址:传送门 socat 不同版本 centos7:传送门 cent…

理解项目开发(寺庙小程序)

转载自&#xff1a;历经一年&#xff0c;开发一个寺庙小程序&#xff01; (qq.com) 破防了&#xff01;为方丈开发一款纪念小程序&#xff01; (qq.com) 下面内容转载自&#xff1a;程序员5K为青岛啤酒节开发个点餐系统&#xff01; (qq.com) 看一个人如何完成一个项目的开发…

SpringBoot集成Swagger的使用

Swagger是一个规范和完整的框架,用于生成、描述、调用和可视化RESTful风格的Web服务。目标是使客户端和文件系统作为服务器以同样的速度来更新文件的方法,参数和模型紧密集成到服务器。 Swagger能够在线自动生成 RESTFul接口的文档&#xff0c;同时具备测试接口的功能。 简单…

UDP的可靠性传输2

系列文章目录 第一章 UDP的可靠性传输-理论篇&#xff08;一&#xff09; 第二章 UDP的可靠性传输-理论篇&#xff08;二&#xff09; 文章目录 系列文章目录三、流量控制RTORTT流量控制1.如何控制流量2. 发送方何时在发送数据3.流程图 拥塞控制1.慢启动 总结1.拥塞控制和流量…

基于Streamlit的应用如何通过streamlit-authenticator组件实现用户验证与隔离

Streamlit框架中默认是没有提供用户验证组件的&#xff0c;大家在基于streamlit快速实现web应用服务过程中&#xff0c;不可避免的需要配置该应用的访问范围和权限&#xff0c;即用户群体&#xff0c;一般的做法有两种&#xff0c;一种是通过用户密码验证机制&#xff0c;要求只…

Matlab图像处理-灰度直方图

一幅含有一个与背景明显对比的物体的灰度图像&#xff0c;如下图所示。 有包含双峰的灰度直方图&#xff0c;如下图所示。 两个尖峰对应于物体内部和外部较多数目的点&#xff0c;两峰尖的谷对应于物体边缘附近相对较少数目的点。在这样的情况下&#xff0c;通常使用直方图来确…

点亮LED——第一个IAR工程

文章目录 说明创建工作区创建新工程向工程添加文件设置工程Options编译工程下载程序EWPtool 插件之前的"测试开发板"章节 测试开发板——第一个AutoSAR程序,使用了一个 demo 工程,不管是裸机程序还是AutoSAR程序,那都是别人已经创建好的工程。本节来介绍如何来创建…

gismo程序示例:边长为 8 16 32 的长方体 受均布载荷

文章目录 前言一、一、8*32面 受均布载荷 二、最小的面&#xff08;8*16&#xff09;受均布载荷三、最大的面受均布载荷 前言 只是为方便学习&#xff0c;不做其他用途&#xff0c; 一、 一、8*32面 受均布载荷 /// This is an example of using the linear elasticity solver…

9月7日扒面经

redis缓存用在哪里&#xff0c;用本地缓存行不行? 数据库查询缓存&#xff0c;减小数据源压力&#xff0c;提高响应速度 页面缓存&#xff1a;将页面的渲染结果缓存在Redis中&#xff0c;以减少页面生成的时间和服务器负载。 频繁计算结果缓存&#xff1a;将频繁计算的结果…

配电房能源监测系统

配电房能源监测系统是一种能够实时监测和管理配电房能源消耗的系统&#xff0c;有助于企业更好地管理能源使用&#xff0c;降低能源成本&#xff0c;提高能源利用效率。本文将详细介绍配电房能源监测系统的组成、功能和优点。 一、配电房能源监测系统的组成 配电房能源监测系统…

2023年8大在线渗透测试工具介绍与分析

随着企业参与数字化运动&#xff0c;网络安全已成为大多数董事会讨论的一个重要方面。事实上&#xff0c;最近的一份报告显示&#xff0c;2022 年网络犯罪造成的损失总额达到惊人的 103 亿美元。 这就是在线渗透测试工具在网络安全中受到关注的地方。 今天&#xff0c;我们希…

【LeetCode-中等题】40. 组合总和 II

文章目录 题目方法一&#xff1a;递归回溯去重 题目 本题需要注意的就是去重操作因为nums数组里面的元素可能存在重复&#xff1a; 不重复的版本&#xff1a;【LeetCode-中等题】39. 组合总和 不去重版 方法一&#xff1a;递归回溯去重 参考讲解视频—回溯算法中的去重&#…

从KOOM看Java内存泄漏检测

前面我们了解了LeakCanary和Matrix Resource Canary中内存泄漏的监控和解析&#xff0c;不难看出LeakCanary是只能在线下部署的&#xff0c;主要原因是因为Debug.dumpHprofData执行会冻结整个应用进程&#xff0c;造成应用进程几秒乃至十多秒不能响应的情况&#xff0c;而dump时…

异步编程 - 10 Web Servlet的异步非阻塞处理

文章目录 OverViewServlet概述Servlet 3.0提供的异步处理能力Servlet 3.1提供的非阻塞IO能力Spring Web MVC的异步处理能力基于DeferredResult的异步处理基于Callable实现异步处理 小结 OverView 我们这里主要讨论Servlet3.0规范前的同步处理模型和缺点&#xff0c;Servlet3.0…

Amazon Aurora MySQL 和 Amazon RDS for MySQL 集群故障转移和只读实例扩容时间测试

01 测试背景 Amazon Aurora MySQL 是与 MySQL 兼容的关系数据库&#xff0c;专为云而打造&#xff0c;性能和可用性与商用数据库相当&#xff0c;成本只有其 1/10。 Amazon RDS for MySQL 让您能够在云中更轻松设置、操作和扩展 MySQL 部署。借助 Amazon RDS&#xff0c;您可以…

小白备战大厂算法笔试(三)——栈、队列、双向队列

文章目录 栈栈常用操作栈的实现基于链表的实现基于数组的实现 两种实现对比栈典型应用 队列队列常用操作队列实现基于链表的实现基于数组的实现 队列典型应用 双向队列双向队列常用操作双向队列实现基于双向链表的实现基于数组的实现 双向队列应用 栈 栈是一种遵循先入后出的逻…