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

接口

接口的概念

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

package main

import (
	"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 infra

import (
	"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 main

import (
	"fmt"
	"learngo/infra"
)

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

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

package main

import (
	"fmt"
	"learngo/infra"
)

func getRetriever() infra.Retriever {
	return infra.Retriever{}
}

func main() {
	var retriever infra.Retriever
	retriever = 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 main

import (
	"fmt"
	"learngo/testing"
)

func getRetriever() testing.Retriever {
	return testing.Retriever{}
}

func main() {
	var retriever testing.Retriever
	retriever = 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 main

import (
	"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 main

import "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 Retriever
	fmt.Println(download(r))
}

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

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

package mock

type Retriever struct {
	Contents string
}

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

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

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

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

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

package real

import (
	"net/http"
	"net/http/httputil"
	"time"
)

type Retriever struct {
	UserAgent string
	TimeOut   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 Retriever
	r = 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 Retriever
	r = 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 queue

type 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 main

import (
	"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 {		// 返回的参数限定为 int
	head := (*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 {
	Retriever
	Poster
}

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

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

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

package mock

type 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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

智能文档解析综述:结构化信息提取的技术、挑战与前景

综述论文:https://arxiv.org/abs/2410.21169 摘要 文档解析对于将非结构化和半结构化文档(如合同、学术论文和发票)转换为结构化、机器可读的数据至关重要。通过从非结构化输入中提取可靠的结构化数据,文档解析为众多应用提供了极…

如何将CSDN博客下载为PDF文件

1.打开CSDN文章内容 2.按键盘上的f12键(或者右键—审查元素)进入浏览器调试模式,点击控制台(Console)进入控制台 3.在控制台输入以下代码,回车 4.在弹出的打印页面中将布局设置成横向,纵向会…

C# GDI绘制的小熊进度条

C# GDI小熊进度条 1、添加自定义控件winform using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms;…

嵌入式入门Day25

数据结构Day 6,IO Day1 查找算法顺序查找折半查找(二分查找)哈希查找 IO概念标准IO创建递归索引(用于查询结构体定义) 文件IO标准IO缓冲区指针相关函数 查找算法 顺序查找 关键字:分为主关键字和次关键字主关键字&am…

操作系统——虚拟内存管理

笔记内容及图片整理自XJTUSE “操作系统” 课程ppt,仅供学习交流使用,谢谢。 背景 进程必须全部放入物理内存后方可运行,这个规则将程序大小限制为物理内存大小。许多情况下并不需要将整个程序置于内存中,比如程序几乎从不执行但…

Java 在Json对象字符串中查找和提取特定的数据

1、在处理JSON数据时,需要提出个别字段的值,通过正则表达式提取特定的数据 public static void main(String[] args) {//定义多个JSON对象字符串类型,假设每个对象有a,b,c 字段String strJson "{\"a\":1.23,\"b\"…

进度与预算

一个项目,如果进度上可以按时完成,一般来说预算不会超标,或者超标幅度有限。 一个项目,如果进度上严重超期,预算基本上会超标,而且超标很大。 现在很多项目,人力成本占比都比较大&#xff0c…

Ungoogled Chromium127编译指南 Windows篇 - 安装Visual Studio 2022(六)

1. 引言 在编译Ungoogled Chromium之前,正确安装和配置Visual Studio 2022是至关重要的一步。作为主要的开发环境,Visual Studio不仅提供了必要的编译工具,还包含了大量构建过程中需要的组件和库。本文将详细介绍如何在Windows系统上安装和配…

电子商务人工智能指南 3/6 - 聊天机器人和客户服务

介绍 81% 的零售业高管表示, AI 至少在其组织中发挥了中等至完全的作用。然而,78% 的受访零售业高管表示,很难跟上不断发展的 AI 格局。 近年来,电子商务团队加快了适应新客户偏好和创造卓越数字购物体验的需求。采用 AI 不再是一…

精确的单向延迟测量:使用普通硬件和软件

论文标题:Precise One-way Delay Measurement with Common Hardware and Software(精确的单向延迟测量:使用普通硬件和软件) 作者信息:Maciej Muehleisen 和 Mazen Abdel Latif,来自Ericsson Research Eri…

字符串的特征

底层是char类型的数组 char[] replace():替换 split():切分 indexOf():第一个字符所在位置,从0开始算 substring(3, 6):字符串截取,包括3不包括6 字符串不可变 本质上是数组,数组是固定值…

三维扫描检测在汽车制造中的应用

三维扫描,通过先进三维扫描技术获取产品和物体的形面三维数据,建立实物的三维图档,满足各种实物3D模型数据获取、三维数字化展示、3D多媒体开发、三维数字化存档、逆向设计、产品开发、直接3D打印制造或辅助加工制造等一系列的应用。 三维扫描…

【已解决】黑马点评项目中启动Spring Boot服务失败,com.sun.tools.javac.tree.JCTree qualid

黑马点评项目中启动Spring Boot服务失败 报错提示 java: java.lang.NoSuchFieldError: Class com.sun.tools.javac.tree.JCTree$JCImport does not have member field com.sun.tools.javac.tree.JCTree qualid这是因为 lombok 版本不兼容造成的 找到 pom.xml 文件&#xff0…

Netty入门(快速了解以及使用netty)

二. Netty 入门 1. 概述 1.1 Netty 是什么? Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients.Netty 是一个异步的、基于事件驱动的网络应用框架&…

Python办公—DataMatrix二维条码制作

目录 专栏导读1、库的介绍2、库的安装3、核心代码4、完整代码总结专栏导读 🌸 欢迎来到Python办公自动化专栏—Python处理办公问题,解放您的双手 🏳️‍🌈 博客主页:请点击——> 一晌小贪欢的博客主页求关注 👍 该系列文章专栏:请点击——>Python办公自动化专…

前缀和(八)矩阵区域和

1314. 矩阵区域和 给你一个 m x n 的矩阵 mat 和一个整数 k &#xff0c;请你返回一个矩阵 answer &#xff0c;其中每个 answer[i][j] 是所有满足下述条件的元素 mat[r][c] 的和&#xff1a; i - k < r < i k, j - k < c < j k 且(r, c) 在矩阵内。 示例 1&…

Nginx日常运维方法Linux版

关注 工 仲 好&#xff1a;IT运维大本营1&#xff0c;安装&#xff1f; 下载RPM&#xff1a;wget http://nginx.org/packages/centos/7/x86_64/RPMS/nginx-1.10.0-1.el7.ngx.x86_64.rpm 离线包用其它方式下载也可以。 安装&#xff1a;rpm -ivh nginx-1.10.0-1.el7.ngx.x86_…

基于eFramework车控车设中间件介绍

车设的发展&#xff0c;起源于汽车工业萌芽之初&#xff0c;经历了机械式操作的原始粗犷&#xff0c;到电子式调控技术的巨大飞跃&#xff0c;到如今智能化座舱普及&#xff0c;远程车控已然成为汽车标配&#xff0c;车设功能选项也呈现出爆发式增长&#xff0c;渐趋多元繁杂。…

【Copilot 】TAB keybinding not working on JetBrains Client

pycharm ssh 远程到ubuntu24.04 发现tab就是tab,无法输出copilot给出的自动补全到便捷器里。禁用host的copilot插件,重新启动ide就好了。解决办法 参考大神的办法删除主机和客户端插件中的 Copilot插件。 仅在客户端中重新安装 Copilot 插件。 我只是禁用也可以 对比了键盘映…

使用API管理Dynadot域名,设置默认域名服务器ip信息

前言 Dynadot是通过ICANN认证的域名注册商&#xff0c;自2002年成立以来&#xff0c;服务于全球108个国家和地区的客户&#xff0c;为数以万计的客户提供简洁&#xff0c;优惠&#xff0c;安全的域名注册以及管理服务。 Dynadot平台操作教程索引&#xff08;包括域名邮箱&…