Go常见错误第15篇:interface使用的常见错误和最佳实践

news2025/7/14 11:49:46

前言

这是Go常见错误系列的第15篇:interface使用的常见错误和最佳实践。

素材来源于Go布道者,现Docker公司资深工程师Teiva Harsanyi。

本文涉及的源代码全部开源在:Go常见错误源代码,欢迎大家关注公众号,及时获取本系列最新更新。

常见错误和最佳实践

interface是Go语言里的核心功能,但是在日常开发中,经常会出现interface被乱用的情况,代码过度抽象,或者抽象不合理,导致代码晦涩难懂。

本文先带大家回顾下interface的重要概念,然后讲解使用interface的常见错误和最佳实践。

interface重要概念回顾

interface里面包含了若干个方法,大家可以理解为一个interface代表了一类群体的共同行为。

结构体要实现interface不需要类似implement的关键字,只要该结构体实现了interface里的所有方法即可。

我们拿Go语言里的io标准库来说明interface的强大之处。io标准库包含了2个interface:

  • io.Reader:表示从某个数据源读数据
  • io.Writer:表示写数据到目标位置,比如写到指定文件或者数据库
Figure 2.3 io.Reader reads from a data source and fills a byte slice, whereas io.Writer writes to a target from a byte slice.

img

io.Reader这个interface里只有一个Read方法:

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

Read reads up to len§ bytes into p. It returns the number of bytes read (0 <= n <= len§) and any error encountered. Even if Read returns n < len§, it may use all of p as scratch space during the call. If some data is available but not len§ bytes, Read conventionally returns what is available instead of waiting for more.

如果某个结构体要实现io.Reader,需要实现Read方法。这个方法要包含以下逻辑:

  • 入参:接受元素类型为byte的slice作为方法的入参。
  • 方法逻辑:把Reader对象里的数据读出来赋值给p。比如Reader对象可能是一个strings.Reader,那调用Read方法就是把string的值赋值给p。
  • 返回值:要么返回读到的字节数,要么返回error。

io.Writer这个interface里只有一个Write方法:

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

Write writes len§ bytes from p to the underlying data stream. It returns the number of bytes written from p (0 <= n <= len§) and any error encountered that caused the write to stop early. Write must return a non-nil error if it returns n < len§. Write must not modify the slice data, even temporarily.

如果某个结构体要实现io.Writer,需要实现Write方法。这个方法要包含以下逻辑:

  • 入参:接受元素类型为byte的slice作为方法的入参。
  • 方法逻辑:把p的值写入到Writer对象。比如Writer对象可能是一个os.File类型,那调用Write方法就是把p的值写入到文件里。
  • 返回值:要么返回写入的字节数,要么返回error。

这2个函数看起来非常抽象,很多Go初级开发者都不太理解,为啥要设计这样2个interface?

试想这样一个场景,假设我们要实现一个函数,功能是拷贝一个文件的内容到另一个文件。

  • 方式1:这个函数用2个*os.Files作为参数,来从一个文件读内容,写入到另一个文件

    func copySourceToDest(source *io.File, dest *io.File) error {
        // ...
    }
    
  • 方式2:使用io.Reader和io.Writer作为参数。由于os.File实现了io.Reader和io.Writer,所以os.File也可以作为下面函数的参数,传参给source和dest。

    func copySourceToDest(source io.Reader, dest io.Writer) error {
        // ...
    }
    

    方法2的实现会更通用一些,source既可以是文件,也可以是字符串对象(strings.Reader),dest既可以是文件,也可以是其它数据库对象(比如我们自己实现一个io.Writer,Write方法是把数据写入到数据库)。

在设计interface的时候要考虑到简洁性,如果interface里定义的方法很多,那这个interface的抽象就会不太好。

引用Go语言设计者Rob Pike在Gopherfest 2015上的技术分享Go Proverbs with Rob Pike中关于interface的说明:

The bigger the interface, the weaker the abstraction.

当然,我们也可以把多个interface结合为一个interface,在有些场景下是可以方便代码编写的。

比如io.ReaderWriter就结合了io.Reader和io.Writer的方法。

type ReadWriter interface {
    Reader
    Writer
}

何时使用interface

下面介绍2个常见的使用interface的场景。

公共行为可以抽象为interface

比如上面介绍过的io.Reader和io.Writer就是很好的例子。Go标准库里大量使用interface,感兴趣的可以去查阅源代码。

使用interface让Struct成员变量变为private

比如下面这段代码示例:

package main
type Halloween struct {
   Day, Month string
}
func NewHalloween() Halloween {
   return Halloween { Month: "October", Day: "31" }
}
func (o Halloween) UK(Year string) string {
   return o.Day + " " + o.Month + " " + Year
}
func (o Halloween) US(Year string) string {
   return o.Month + " " + o.Day + " " + Year
}
func main() {
   o := NewHalloween()
   s_uk := o.UK("2020")
   s_us := o.US("2020")
   println(s_uk, s_us)
}

变量o可以直接访问Halloween结构体里的所有成员变量。

有时候我们可能想做一些限制,不希望结构体里的成员变量被随意访问和修改,那就可以借助interface。

type Country interface {
   UK(string) string
   US(string) string
}
func NewHalloween() Country {
   o := Halloween { Month: "October", Day: "31" }
   return Country(o)
}

我们定义一个新的interface去实现Halloween的所有方法,然后NewHalloween返回这个interface类型。

那外部调用NewHalloween得到的对象就只能使用Halloween结构体里定义的方法,而不能访问结构体的成员变量。

乱用Interface的场景

interface在Go代码里经常被乱用,不少C#或者Java开发背景的人在转Go的时候,通常会先把接口类型抽象好,再去定义具体的类型。

然而,这并不是Go里推荐的。

Don’t design with interfaces, discover them.

—Rob Pike

正如Rob Pike所说,不要一上来做代码设计的时候就先把interface给定义了。

除非真的有需要,否则是不推荐一开始就在代码里使用interface的。

最佳实践应该是先不要想着interface,因为过度使用interface会让代码晦涩难懂。

我们应该先按照没有interface的场景去写代码,如果最后发现使用interface能带来额外的好处,再去使用interface。

注意事项

使用interface进行方法调用的时候,有些开发者可能遇到过一些性能问题。

因为程序运行的时候,需要去哈希表数据结构里找到interface的具体实现类型,然后调用该类型的方法。

但是这个开销是很小的,通常不需要关注。

总结

interface是Go语言里一个核心功能,但是使用不当也会导致代码晦涩难懂。

因此,不要在写代码的时候一上来就先写interface。

要先按照没有interface的场景去写代码,如果最后发现使用interface真的可以带来好处再去使用interface。

如果使用interface没有让代码更好,那就不要使用interface,这样会让代码更简洁易懂。

推荐阅读

  • Go面试题系列,看看你会几题?

  • Go常见错误第1篇:未知枚举值

  • Go常见错误第2篇:benchmark性能测试的坑

  • Go常见错误第3篇:go指针的性能问题和内存逃逸

  • Go常见错误第4篇:break操作的注意事项

  • Go常见错误第5篇:Go语言Error管理

  • Go常见错误第6篇:slice初始化常犯的错误

  • Go常见错误第7篇:不使用-race选项做并发竞争检测

  • Go常见错误第8篇:并发编程中Context使用常见错误

  • Go常见错误第9篇:使用文件名称作为函数输入

  • Go常见错误第10篇:Goroutine和循环变量一起使用的坑

  • Go常见错误第11篇:意外的变量遮蔽(variable shadowing)

  • Go常见错误第12篇:如何破解箭头型代码

  • Go常见错误第13篇:init函数的常见错误和最佳实践

  • Go常见错误第14篇:过度使用getter和setter方法

开源地址

文章和示例代码开源在GitHub: Go语言初级、中级和高级教程。

公众号:coding进阶。关注公众号可以获取最新Go面试题和技术栈。

个人网站:Jincheng’s Blog。

知乎:无忌。

福利

我为大家整理了一份后端开发学习资料礼包,包含编程语言入门到进阶知识(Go、C++、Python)、后端开发技术栈、面试题等。

关注公众号「coding进阶」,发送消息 backend 领取资料礼包,这份资料会不定期更新,加入我觉得有价值的资料。

发送消息「进群」,和同行一起交流学习,答疑解惑。

References

  • https://livebook.manning.com/book/100-go-mistakes-how-to-avoid-them/chapter-2/
  • https://github.com/jincheng9/go-tutorial/tree/main/workspace/lesson18
  • https://bbs.huaweicloud.com/blogs/348512

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

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

相关文章

加载配置文件内容利用反射动态创建对象和调用方法(开闭原则的体现)

反射的应用&#xff1a;根据配置文件来创建对象和调用方法 需求&#xff1a;1&#xff0c;根据配置文件re.properties 指定的信息&#xff0c;创建对象并调用方法 classfullpathsrc.com.liu.Cat methodhi 即通过外部文件配置&#xff0c;在不修改源码的情况下&#xff0c;来控…

GAN详解

前言 GAN是当今作为火热的生成式算法&#xff0c;由Ian Goodfellow&#xff0c;Yoshua Bengio等人在2014年提出的&#xff0c;Yan LeCun表示道GAN是“adversarial training is the coolest thing since sliced bread”。它使用两个神经网络&#xff0c;将一个神经网络与另一个…

ArcGIS中ArcMap栅格图层0值设置为NoData值的简便方法

本文介绍在ArcMap软件中&#xff0c;将栅格图层中的0值或其他指定数值作为NoData值的方法。 在处理栅格图像时&#xff0c;有时会发现如下图所示的情况——我们对某一个区域的栅格数据进行分类着色后&#xff0c;其周边区域&#xff08;即下图中浅蓝色的区域&#xff09;原本应…

C语言学习记录(十五)C预处理器和C库

文章目录一、C预处理器1.1 翻译程序1.2 明示常量&#xff1a;#define1.3 在#define中使用参数1.4 文件包含&#xff1a;#include1.5 其他指令1.5.1 #undef指令1.5.2 从C预处理角度看已定义1.5.3 条件编译1.5.3.1 #ifdef、#else和#endif指令1.5.3.2 #ifndef1.5.3.3 #if和elif指令…

Gof23-创建型-工厂-单例-抽象工厂-建造-原型以及UML的绘制

创建型的设计模式工厂模式单例模式抽象工厂建造者模式原型模式UML图形的绘制工厂模式 工厂模式 Factory Pattern 适用的场景&#xff1a;统一的接口作为统一的零件&#xff0c;实现类作为零件的组合&#xff0c;将实例产品类的生产交给工厂&#xff0c;用户只需要面对工程提取…

XML的创建和读取

rapidxml是一个快速的xml库&#xff0c;由C模板实现的高效率xml解析库&#xff0c;同时也是boost库的property_tree的内置解析库。 当使用rapidxml时&#xff0c;只需要把rapidxml.hpp 、 rapidxml_print.hpp 和 rapidxml_utils.hpp 三个文件拷贝到你的工程目录下&#xff0c;就…

Pytorch中KL loss

1. 概念 KL散度可以用来衡量两个概率分布之间的相似性&#xff0c;两个概率分布越相近&#xff0c;KL散度越小。 上述公式表示P为真实事件的概率分布&#xff0c;Q为理论拟合出来的该事件的概率分布。D(P||Q)&#xff08;P拟合Q&#xff09;和D(Q||P)&#xff08;Q拟合P&…

ajax之Content-Type示例

参考资料: Content-Type详解【SpringBoot】SpringBoot接收请求的n种姿势 目录前期准备0. Content-Type概念解释1. application/x-www-form-urlencoded1.1 form表单示例1.2 jQuery的ajax示例2. application/json2.1 指定contentType为json,不使用RequestBody接收2.2 不指定cont…

01 OSI七层网络排查 troubleshooting 思路及对应工具

文章目录1 .前言2. OSI 的七层模型&#xff0c;和 TCP/IP 的四层 / 五层模型区别2.1 网络专业术语2.2 TLS 解释2.3 什么是TCP 流&#xff1f;3. 网络各层排查工具3.1 应用层3.1.1 浏览器的开发者工具3.1.1.1 找到有问题的服务端IP3.1.1.2 辅助排查网页慢的问题3.1.1.3 解决失效…

嵌入式数据库sqlite3

一、数据库 数据库的基本概念 常用的数据库 大型数据库 Oracle公司是最早开发关系数据库的厂商之一&#xff0c;其产品支持最广泛的操作系统平台。目前Oracle关系数据库产品的市场占有率名列前茅。 IBM 的DB2是第一个具备网上功能的多媒体关系数据库管理系统&#xff0c;支…

bootstrap学习(四)

bootstrap中图片、按钮、表单 按钮&#xff1a; 不加样式的按钮&#xff1a; 在bootstrap中a标签也可以生成按钮&#xff1a; 默认按钮尺寸可以不加&#xff0c;它是自动显示默认尺寸 加btn-block&#xff1a;class 图片&#xff1a; 表单&#xff1a; 垂直表单&#xff1a;…

【语音识别】MFCC+VAD端点检测智能语音门禁系统【含GUI Matlab源码 451期】

⛄一、MFCC简介 1 引言 语音识别是一种模式识别, 就是让机器通过识别和理解过程把语音信号转变为相应的文本或命令的技术。语音识别技术主要包括特征提取技术、模式匹配准则及模型训练技术3个方面。目前一些语音识别系统的适应性比较差, 主要体现在对环境依赖性强, 因此要提高…

[mysql] 深入分析MySQL版本控制MVCC规则--实测 (mysql 8.0 innodb引擎)

背景&#xff1a;基于之前的一篇文章 可重复读&#xff1a;可重复读隔离级别的实现是每个事务在打开时都会生成一个一致的视图。 当其他事务提交时&#xff0c;不会影响当前事务中的数据。 为了保证这一点&#xff0c;MySQL是通过多版本控制机制MVCC来实现的&#xff1b; 我们亲…

Go语言面试题合集(2022)

基础语法 Go 支持默认参数或可选参数吗&#xff1f; 不支持。但是可以利用结构体参数&#xff0c;或者…传入参数切片数组。 // 这个函数可以传入任意数量的整型参数 func sum(nums ...int) {total : 0for _, num : range nums {total num}fmt.Println(total) }Go 语言 tag…

pandas算术运算、逻辑运算、统计运算describe()函数、统计函数、累计统计函数及自定义函数运算

一、pandas算术运算 直接对数据进行加、减、乘、除等运算&#xff0c;可使用函数add()、sub()、mul()、div()或、-、、 代码如下 数据生成 import pandas as pd import numpy as np# 数据生成代码 num np.random.randint(50, 100, (3, 5))# 传入标签索引 column [第一列, …

[hadoop全分布部署]安装Hadoop、配置Hadoop 配置文件①

&#x1f468;‍&#x1f393;&#x1f468;‍&#x1f393;博主&#xff1a;发量不足 个人简介&#xff1a;耐心&#xff0c;自信来源于你强大的思想和知识基础&#xff01;&#xff01; &#x1f4d1;&#x1f4d1;本期更新内容&#xff1a;安装Hadoop、配置Hadoop 配置文件…

基于SSM的高校课程评价系统

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;Vue 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#xff1a;是 目录…

一文带你深入理解【Java基础】· 注解

写在前面 Hello大家好&#xff0c; 我是【麟-小白】&#xff0c;一位软件工程专业的学生&#xff0c;喜好计算机知识。希望大家能够一起学习进步呀&#xff01;本人是一名在读大学生&#xff0c;专业水平有限&#xff0c;如发现错误或不足之处&#xff0c;请多多指正&#xff0…

多线程编程【条件变量】

条件变量&#x1f4d6;1. 为什么需要条件变量&#xff1f;&#x1f4d6;2. 条件变量概念&#x1f4d6;3. 发信号时总是持有锁&#x1f4d6;4. 生产者消费者问题&#x1f4d6;5. 基于阻塞队列的生产者消费者模型&#x1f4d6;1. 为什么需要条件变量&#xff1f; 在很多情况下&a…

Android开发音效增强中铃声播放Ringtone及声音池调度SoundPool的讲解及实战(超详细 附源码)

需要源码请点赞关注收藏后评论区留下QQ~~~ 一、铃声播放 虽然媒体播放器MediaPlayer既可用来播放视频&#xff0c;也可以用来播放音频&#xff0c;但是在具体的使用场合&#xff0c;MediaPlayer存在某些播音方面的不足之处 包括以下几点 1&#xff1a;初始化比较消耗资源 尤其…