【Golang】Go语言编程思想(一):接口

接口

接口的概念

现在我们要实现一个函数,用于对给定的 url 进行解析,具体的代码实现如下:

package mainimport ("fmt""io""net/http"
)func retrieve(url string) string {resp, err := http.Get(url)if err != nil {panic(err)}defer resp.Body.Close()bytes, _ := io.ReadAll(resp.Body)return string(bytes)
}func main() {url := "https://www.baidu.com"fmt.Println(retrieve(url))
}

现在我们假设在写一个较大的工程,有一个专门负责网络架构的团队来完成网络请求、磁盘读写等需求的实现,保存在目录 infra 下,其中实现了一个 Retriever 保存在 urlretriever.go 文件下。

Retriever 的结构和方法的具体实现如下:

package infraimport ("io""net/http"
)type Retriever struct{}func (Retriever) Get(url string) string {// 接收者在不需要名字的时候可以只写类型resp, err := http.Get(url)if err != nil {panic(err)}defer resp.Body.Close()bytes, _ := io.ReadAll(resp.Body)return string(bytes)
}

此时在 main 函数中,只需要新建一个 infra.Retriever 类型并使用其方法 Get 即可完成与上述代码等价的需求:

package mainimport ("fmt""learngo/infra"
)func main() {retriever := infra.Retriever{}url := "https://www.baidu.com"fmt.Println(retriever.Get(url))
}

上述代码还可以进一步地工程化,因为在大型项目当中,可能不止网络开发团队实现了 Retriever,测试团队可能同样开发了 Retriever 用于这部分代码的测试。所以显式的 infra.Retriever{ } 可以使用函数进行代替:

package mainimport ("fmt""learngo/infra"
)func getRetriever() infra.Retriever {return infra.Retriever{}
}func main() {var retriever infra.Retrieverretriever = getRetriever()url := "https://www.baidu.com"fmt.Println(retriever.Get(url))
}

此时我们注意到,上述代码还是不够好,因为就算将 infra.Retriever{ } 放到函数当中,main 函数当中的 retriever 的类型仍然是显式或隐式为 infra.Retriever 的。

瑕疵在于,main 函数当中的 retriever 必须是 infra.Retriever 类型的,main 函数与 infra.Retriever 的耦合较深。如果想要解耦,就需要用到 Go 的接口(interface)。

假如此时测试团队的 testing 目录下也有一个 Retriever,它同样有一个 Get 方法,但是行为与 infra 的 Retriever 完全不同,此时我们无法对 main 文件当中的 getRetriever 函数的返回类型进行修改。或者说,更换一个 Retriever,需要修改许多个地方,造成了很多工作量上的冗余。

package mainimport ("fmt""learngo/testing"
)func getRetriever() testing.Retriever {return testing.Retriever{}
}func main() {var retriever testing.Retrieverretriever = getRetriever()url := "https://www.baidu.com"fmt.Println(retriever.Get(url))
}

产生上述我们不满意的情况的原因是,Golang 是一个强类型的语言(或者说静态语言),而不是弱类型或动态绑定的系统,在写代码时,编译阶段我们就已经知道变量的类型,而对于 Python 等动态语言,在运行时才知道类型。

解决上述问题的方案是,让代码与逻辑相一致。在 main 函数中,变量 retriever 的类型不应该强绑定为某个类型的 retriever:

var retriever ?
// ? is something that can get

retriever 的类型假设我们此时是不知道的,但是我们需要这个类型具有 Get 方法,才能使后面的:

retriever.Get(url)

顺利运行。至于具体的类型是 infra 的 Retriever 还是 testing 的 Retriever,我们不需要关心。这个我们不知道的类型正是接口(interface)。

使用关键字 interface 来定义一个接口,它的声明语句与 struct 的声明非常类似:

type retriever interface {Get(string) string
}

在这里插入图片描述

使用接口的具体实现如下:

package mainimport ("fmt""learngo/testing"
)func getRetriever() testing.Retriever {return testing.Retriever{}
}type retriever interface {Get(string) string
}func main() {var r retriever = getRetriever()url := "https://www.baidu.com"fmt.Println(r.Get(url))
}

函数 getRetriever() 的返回值类型也应该与 testing 解耦,直接更换为接口 retriever:

func getRetriever() retriever {return testing.Retriever{}	// 返回的仍然是 testing.Retriever{}
}

使用 testing 的 Retriever 测试通过后,可以将代码业务上限,此时只需要更换 getRetriever 中的 testing 为 infra 即可。

鸭子类型(duck typing)

鸭子类型可以概括为:走路像鸭子,说话像鸭子,长得像鸭子,那么它就是鸭子。它强调的是描述事物的外部行为而非内部结构。

严格说 Go 属于结构化类型系统,类似 Duck Typing。

接口的定义和实现

接口由使用者来定义。
在这里插入图片描述
下面是一个示例,此例需要我们新建一个 retriever 目录,并将下述代码写在目录下的 main.go 当中:

package mainimport "fmt"type Retriever interface {Get(url string) string
}func download(r Retriever) string {// download 是一个使用者, 使用者要 Get, 因此要定义一个 Retriever 接口return r.Get("https://www.baidu.com")
}func main() {var r Retrieverfmt.Println(download(r))
}

但是上述程序还无法直接运行,因为 r 还没有一个具体的实现。

在 retriever 目录下新建 mock 目录,并在 mock 目录下定义 mockretriever.go:

package mocktype Retriever struct {Contents string
}func (r Retriever) Get(url string) string {return r.Contents
}

编译器会发现我们定义的是可以用于接口实现的结构:
在这里插入图片描述
此时,修改我们的 main 函数体为:

func main() {var r Retrieverr = mock.Retriever{"this is a fake www.baidu.com"}fmt.Println(download(r))
}

👆此时,我们定义了一个接口的实现,相当于把一个可以匹配接口的结构对象赋予接口对象。

按照类似的方式,我们在 retriever 目录下新建一个 real 目录,在当中实现真实的 Retriever,其结构实现定义在 retriever.go 文件中:
在这里插入图片描述
Retriever 结构的定义和方法如下:

package realimport ("net/http""net/http/httputil""time"
)type Retriever struct {UserAgent stringTimeOut   time.Duration // 代表时间长度
}func (r *Retriever) Get(url string) string {resp, err := http.Get(url)if err != nil {panic(err)}result, err := httputil.DumpResponse(resp, true)resp.Body.Close()if err != nil {panic(err)}return string(result)
}

在 main.go 中使用 real 的 Retriever 实现接口:

func main() {r := real.Retriever{}fmt.Println(download(r))
}

可以看到,由使用者(对应上述 main.go 当中的 download)来定义接口当中必须要有的方法,实现者不需要实现某个具体的接口,只需要实现接口当中的方法即可。

接口的值类型

现在我们想要查看接口当中究竟包含哪些成员,对 main 函数体进行如下修改:

func main() {var r Retrieverr = mock.Retriever{"this is a fake www.baidu.com"}fmt.Printf("%T %v\n", r, r)r = real.Retriever{}fmt.Printf("%T %v\n", r, r)fmt.Println(download(r))
}

输出的内容如下:

mock.Retriever {this is a fake www.baidu.com}
real.Retriever { 0s}	// UserAgent 是 空格, TimeOut 是 0s

由于 Golang 的函数调用均为传值调用,当我们为 real.Retriever 定义成员时,使用指针接收者对方法进行定义可以加快方法的速度。如果单纯地将结构的方法改为指针接收者:func (r *Retriever) Get(url string) string { ... }

则接口使用的部分将会报错:
在这里插入图片描述
取一个地址即可修改上述错误:
在这里插入图片描述
此时运行 main 函数得到的结果如下:

mock.Retriever {this is a fake www.baidu.com}
*real.Retriever &{Mozilla/5.0 1m0s}

因此,接口接收的可能是一个真实的值,也可能是一个指针。如果接收一个指针,那么指针指向的对象在实现接口的方法时应该使用指针接收者。

我们还想要知道接口当中结构的类型,获取接口中结构的类型有多种方法,定义一个 inspect 函数来完成,inspect 的参数是 Retriever 接口:

func inspect(r Retriever) {switch v := r.(type) {case mock.Retriever:fmt.Printf("Contents:", v.Contents)case *real.Retriever:fmt.Printf("UserAgent:", v.UserAgent)}
}

修改 main 为:

func main() {var r Retrieverr = mock.Retriever{"this is a fake www.baidu.com"}inspect(r)r = &real.Retriever{UserAgent: "Mozilla/5.0",TimeOut:   time.Minute,}inspect(r)//fmt.Println(download(r))
}

结果如下:

mock.Retriever {this is a fake www.baidu.com}
Contents: this is a fake www.baidu.com
*real.Retriever &{Mozilla/5.0 1m0s}
UserAgent: Mozilla/5.0

此外,我们还可以通过 Type Assertion 的方法来获取 interface 当中的类型:

// Type Assertion
realRetriever := r.(*real.Retriever)
fmt.Println(realRetriever.TimeOut)
/* ... 修改 r 的类型为 mock.Retriever ... */
mockRetriever := r.(mock.Retriever)		// 注意, type assertion 必须添加圆括号, 无论是值还是指针
fmt.Println(mockRetriever.Contents)

通过上面的例子,可以将接口变量当中包含的内容总结为:实现者的类型 + 实现者的值/指针:
在这里插入图片描述

  • 接口变量自带指针;
  • 接口变量同样采用值传递,几乎不需要使用接口的指针;
  • 指针接收者的方法实现只能以指针方式使用,值接收者都可以。(正如上面 real.Retriever 的例子,该例将方法定义为指针接收者,在使用接口接收 real.Retriever 时,必须取 real.Retriever 的地址,否则会编译出错。而 mock.Retriever 对接口方法的实现使用值接收者,我们可以传递 mock.Retriever 的地址给接口,这样做不会产生编译错误,而直接使用 mock.Retriever 的值则更加方便。)

可以使用 interface{} 来表示任何类型

可以使用 interface{} 来表示任何类型,一个例子如下,该例尝试对我们之前实现的 queue 进行修改。先前实现的 queue 是对 []int 的别名,即 int 类型的 slice 的别名,并对 queue 定义了许多方法。如果我们不希望 queue 只能接受 int 类型的值,可以使用 interface{} 来对其进行改写:

package queuetype Queue []interface{} // 使用 interface{} 表示任何类型func (q *Queue) Push(v interface{}) {*q = append(*q, v) // q 指向的 slice 被改变了
}func (q *Queue) Pop() interface{} {head := (*q)[0]*q = (*q)[1:]return head
}func (q *Queue) IsEmpty() bool {return len(*q) == 0
}

此时的 Queue 类似于 Python,可以接受任何类型的变量。

此时使用 Queue:

package mainimport ("fmt""learngo/queue"
)func main() {q := queue.Queue{1}q.Push(2)q.Push(3)fmt.Println(q.Pop())fmt.Println(q.Pop())fmt.Println(q.IsEmpty())fmt.Println(q.Pop())fmt.Println(q.IsEmpty())q.Push("abc")q.Push(3.1415926)fmt.Println(q.Pop())fmt.Println(q.Pop())
}

输出为:

1
2
false
3
true
abc
3.1415926

如果我们想要对 Queue 进行进一步的限定,使得它只能接受 int 类型的变量,可以进一步对 Queue 的定义进行修改:

type Queue []interface{} // 使用 interface{} 表示任何类型func (q *Queue) Push(v int) {	// 传入的参数限定为 int*q = append(*q, v) // q 指向的 slice 被改变了
}func (q *Queue) Pop() int {		// 返回的参数限定为 inthead := (*q)[0]*q = (*q)[1:]return head.(int) // 需要将返回值强制类型转换为 int// 👆 head 是一个 interface, 将 interface 当中的值强制转换为 int
}func (q *Queue) IsEmpty() bool {return len(*q) == 0
}

接口的组合

有时我们希望一个接口既可以读又可以写,此时可以用到接口的组合。

首先我们定义一个 Poster 接口,将其一并定义在 retriever 目录下的 main.go 文件当中(与 Retriever 接口定义在同一个文件当中),并定义一个接口要执行的行为(放在函数 post 当中,正如 Retriever 接口要做的事情放在了 download 当中):

type Poster interface {Post(url string, form map[string]string) string
}func post(poster Poster) {poster.Post("https://www.baidu.com",map[string]string{"name": "baidu","item": "BaiduNetDisk",})
}

我们希望定义一个既可以读也可以写的接口,可以将上述的 Retriever 接口和 Poster 接口定义在 RetrieverPoster 接口当中来完成接口的组合:

type RetrieverPoster interface {RetrieverPoster
}

当然,RetrieverPoster 当中可以定义其它的方法,但此时还用不上,只需要 Retriever 和 Poster 两个接口。进一步将接口 RetrieverPoster 要做的事情定义在 session 当中:

func session(s RetrieverPoster) string {s.Get()s.Post()
}

在具体的实现部分,以 mock.Retriever 为例,如果想要 RetrieverPoster 接受 mock.Retriever,则它还需要再实现一个名为 Post 的接口:

package mocktype Retriever struct {Contents string
}func (r Retriever) Post(url string, form map[string]string) string {	// 实现 Post 方法r.Contents = form["contents"]										// 为了将结构传入 RetrieverPoster 接口return "ok"
}func (r Retriever) Get(url string) string {return r.Contents
}

此时给 session 一个具体的实现:

func session(s RetrieverPoster) string {s.Post(url, map[string]string{"contents": "another faked www.baidu.com",})return s.Get(url)
}

现在在 main 函数体当中,将 mock.Retriever 传递给 RetrieverPoster 接口:

func main() {r := mock.Retriever{"this is a fake www.baidu.com"}fmt.Println()fmt.Println(session(r))
}

结果为:

this is a fake www.baidu.com

与我们的预期不符,原因是 session 当中修改了 Contents 的内容,而结果没有显示修改后的值。原因在于最初定义的 Post 使用的是值接受者,需要对 mock.Retriever 的两个接口方法的定义进行进一步的修改,将 Post 和 Get 方法修改为指针接收者。修改后的结果为:

func (r *Retriever) Post(url string, form map[string]string) string {r.Contents = form["contents"]return "ok"
}func (r *Retriever) Get(url string) string {return r.Contents
}

可以预见的是,在 main 函数体中,也需要把 RetriverPoster 的接收值修改为地址:

// ...
r := &mock.Retriever{"this is a fake www.baidu.com"}
// ...

此时得到的是正确的值:

another faked www.baidu.com

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

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

相关文章

SAP SD 如何设置交货单数量可修改为0

在日常运维中,销售订单可以被reject,但是交货单只能被物理删除 但是粗暴的物理删除,又会使得单据不连续,出现问题不好追溯 所以我们就可以通过将废弃的交货单的数量置为0 配置如下: C表示,创建的时候不可…

记一次由docker容器使得服务器cpu占满密码和密钥无法访问bug

Bug场景: 前几天在服务器上部署了一个免费影视网站,这个应用需要四个容器,同时之前的建站软件workpress也是使用docker部署的,也使用了三个容器。在使用workpress之前,我将影视软件的容器全部停止。 再使用workpress…

Matlab R2024b 中文版 下载及安装教程

点击下方链接下载安装包 Matlab R2024b 中文版安装包点击下载https://mp.weixin.qq.com/s/Kq2j1dQLdULOVV9vrA6pkA 安装教程 1.通过上方链接下载软件,鼠标右键【MATLAB R2024b(64bit)】压缩包,选择解压到MATLAB R2024b(64bit)。 2.双击进入解压后的文…

2024年12月6日Github流行趋势

项目名称:lobe-chat 项目维护者:arvinxx, semantic-release-bot, canisminor1990, lobehubbot, renovate项目介绍:一个开源的现代化设计的人工智能聊天框架。支持多AI供应商(OpenAI / Claude 3 / Gemini / Ollama / Qwen / DeepSe…

韩企研学团造访图为科技:共探人工智能创新前沿

今日,一支由韩国知名企业研学专家组成的代表团莅临图为科技深圳总部,展开了一场深度技术交流与研讨活动。 此次访问旨在通过实地探访中国领先的科技企业,促进中韩两国在科技创新领域的深入合作与交流。 韩国游学团合影 图为科技作为一家在人…

Vulnhub---kioptirx4 udf手工提权

个人博客 WuTongSec 打点 nmap -sP 192.168.128.0/24 找机器 nmap -P- 192.168.128.135 端口快扫 nmap -min-rate 10000 -sV -sC -O 192.168.128.135 脚本并没有扫出 什么洞 dirsearch -u http://192.168.128.135 目录扫描 三个200 那就先上web看看 web是应该登录框 在pas…

基于RISC-V的HSM方案

安全之安全(security)博客目录导读 本篇博客,我们聚焦RISC-V 2024中国峰会上RISC-V的一个HSM(Hardware Security Module)实现方案,来自芯来科技王松老师。 关于RISC-V TEE(可信执行环境)的相关方案,如感兴趣可参考RIS…

【C++探索学习】第十九弹——进程替换:深入解析操作系统中的进程替换机制

Linux学习笔记: https://blog.csdn.net/2301_80220607/category_12805278.html?spm1001.2014.3001.5482 前言: 在Linux操作系统中,进程替换(Process Replacement)是一个重要的概念,它允许程序通过系统调…

[软件工程]八.软件演化

8.1什么是软件演化 由于种种不可避免的原因,系统开发完成后的软件需要进行修改来适应变更的需求,我们对软件的修改就叫软件演化。 8.2为什么软件会演化 由于业务的变更或者为了满足用户期待的改变,使得对已有的系统的新需求浮现出来。由于…

WiFi受限不再愁,电脑无网络快速修复指南

有时在试图连接WiFi时,会发现网络连接受限,或无法正常访问互联网。这种情况不仅影响了工作效率,还可能错过重要的信息。那么,究竟是什么原因导致了电脑WiFi连接受限呢?又该如何解决这一问题呢?小A今天就来教…

【Liunx篇】基础开发工具 - yum

文章目录 🌵一.Liunx下安装软件的方案🐾1.源代码安装🐾2.rpm包安装🐾3.包管理器进行安装 🌵二.软件包管理器-yum🌵三.yum的具体操作🐾1.查看软件包🐾2.安装软件包🐾3.卸载…

第七节(2)、T型加减速优化处理【51单片机-TB6600驱动器-步进电机教程】

摘要:本节介绍解决标准T型加减速过程中的两个缺陷,其一是使得初速度任意设置;其二是降低Cn递推计算量,提升速度上限 一. 加速减速过程计算 1.1计算不存在匀速过程 根据基本运动定理: w m a x w 0 a 0 ∗ t n 0 … …

什么是继承性?C++中如何实现继承? 继承的好处和注意事项有哪些?

1) 什么是继承性?C中如何实现继承? 一、继承性的概念 继承性是面向对象编程中的一个重要特性,它允许一个类(称为子类或派生类)继承另一个类(称为父类或基类)的属性和方法。继承的主要目的是实现…

唇形同步视频生成工具:Wav2Lip

一、模型介绍 今天介绍一个唇形同步的工具-Wav2Lip;Wav2Lip是一种用于生成唇形同步(lip-sync)视频的深度学习算法,它能够根据输入的音频流自动为给定的人脸视频添加准确的口型动作。 (Paper) Wav2Lip模型…

ESP32使用TCA9548A IIC多路复用器拓展

ESP32使用TCA9548A IIC多路复用器拓展 本文将讲述如何使用TCA9458A IIC多路复用器扩展IIC总线端口。使用相同的IIC地址控制多个IIC设备,如多个OLED或BME280等 TCA9548介绍 IIC通信协可以实现在同一IIC总线上与多个IIC设备通信,只要所有设备都具有唯一…

BGP路径属性与路由反射器

BGP路径属性 路径属性: 任何一条BGP路由都拥有多个路径属性 当路由器将BGP路由通告给它的对等体时,一并被通告的还有路由所携带的各各路径属性 BGP的路径属性将影响路由优选 路径四个属性分类: 公认必遵:必须包括在每个upda…

C语言期末考试——重点考点

目录 1.C语言的结构 2.三种循环结构 3.逻辑真假判断 4. printf函数 5. 强制类型转化 6. 多分支选择结构 7. 标识符的定义 8. 三目运算符 1.C语言的结构 选择结构、顺序结构、循环结构 2.三种循环结构 for、while、do-while 3.逻辑真假判断 C语言用0表示false,用非0(不…

ci/cd配置任务超时时间

有两个地方决定了任务超时时间: 1. 2.gitlab-runner

JUC:Synchronized和锁升级

1. 面试题 谈谈你对Synchronized的理解Sychronized的锁升级你聊聊Synchronized实现原理,monitor对象什么时候生成的?知道monitor的monitorenter和monitorexit这两个是怎么保证同步的嘛?或者说这两个操作计算机底层是如何执行的偏向锁和轻量级…

梯度下降法以及 Python 实现

文章目录 1. 引言2. 梯度法3. 例子4. 代码实现5. 讨论 — 学习率 η \eta η5.1 当 η \eta η 设置过大5.2 当 η \eta η 设置过小 参考 1. 引言 梯度下降法,可以根据微分求出的斜率计算函数的最小值。 在人工智能中,经常被应用于学习算法。 2. 梯…