Go:建议你使用专门的构造函数
争做团队核心程序员,关注「幽鬼」
大家好,我是程序员幽鬼。
Go 语言并非完全的面向对象语言,只是有部分面向对象特性。其中,没有实际意义的构造函数,但对类似构造函数有自己的一些约定成俗的规则。本文讲解为什么在 Go 中,建议你尽量使用专门的构造函数。
01 Go 和 Struct 初始化的背景
Go 有一个特殊的特性。它不同于其他语言,Go 的构造函数也是独一无二的。我们通过比较其他语言如何构造对象来建立基准。
PHP
<?php
class Point {
protected int $x;
protected int $y;
public function __construct(int $x, int $y = 0) {
$this->x = $x;
$this->y = $y;
}
}
$p1 = new Point(4, 5);
Java 重载(多个构造函数)
public class MyClass {
private int number = 0;
public MyClass() {
}
public MyClass(int theNumber) {
this.number = theNumber;
}
}
Solidity
pragma solidity ^0.5.0;
contract Base {
uint data;
constructor(uint _data) public {
data = _data;
}
}
Go 呢?
Go 语言不强制构造函数(没有语法层面的构造函数)。它经常通过 “复合字面量” 来实例化结构体。
复合字面量为结构体、数组、切片和 map 构造值,并在每次计算(evaluated)它们时创建一个新值。
它们由字面值类型后跟花括号绑定的元素列表组成。相应的键可以可选地位于每个元素之前。
复合字面量示例:
type Point3D struct { x, y, z float64 }
type Line struct { p, q Point3D }
可以这么初始化:
origin := Point3D{} // zero value for Point3D
line := Line{origin, Point3D{y: -4, z: 12.3}} // zero value for line.q.x
“作为一种限制情况,如果复合字面量根本不包含任何字段,它会为该类型创建一个零值。表达式new(Point3D)
和&Point3D{}
是等效的。”
你可以将其记住为,如果未提供参数,则各字段使用其类型的默认值。Type + Curly Brackets.
Go 的设计考虑了简单性和无样板代码,复合字面量正是如此,但它们**不必要地难以维护。
为什么?让我用复合字面量解释我的日常问题以及我喜欢如何解决它们。
02 创建构造函数保持代码的可维护性
我的构造函数规则:
当创建一个新的结构体,比如
Transaction(Tx)
,我马上创建一个专用的构造器函数:NewTx()
。
type Tx struct {
From common.Address `json:"from"`
To common.Address `json:"to"`
Gas uint `json:"gas"`
GasPrice uint `json:"gasPrice"`
Value uint `json:"value"`
Nonce uint `json:"nonce"`
Data string `json:"data"`
Time uint64 `json:"time"`
}
func NewBaseTx(from, to common.Address, value uint, nonce uint, data string) Tx {
return Tx{from, to, TxGas, TxGasPriceDefault, value, nonce, data, uint64(time.Now().Unix())}
}
这么做,我认为有 4 点原因,相信对你会有帮助。
原因 1 - 有些没有合适的默认值
没有构造函数
假设我没有创建专用的构造函数 NewBaseTx()
。如果我必须向 Tx 结构添加另一个属性MaxGasPrice
,我将不得不修改数十/数百个文件(取决于代码库大小/实现)。我这里没有夸大其词。这是一个非常现实的估计,因为我已经在各种项目中遇到过几次这样的情况。
字段:值构造函数
但是,如果我将构造函数元素标记为“显式 字段:值 对,初始化程序可以按任何顺序出现,缺少的当作零值”,重构可能很快以生产错误告终,因为编译器不会指出我忘记传递新值的所有地方;所以这是更糟糕的方法。
MaxGasPrice
默认情况下会意外地为 0,从而使结构变为无效状态。甚至不要尝试使用可选的 setter 来修复它。
通常,我在field:value
构造函数中看不到任何值,因为任何好的 IDE 都会像我之前的屏幕截图一样以图形方式向你显示名称。你有什么经验?同意还是您有不同的看法?
专用构造函数
幸运的是,我可以避免所有这些混乱,并为我的队友避免大量 PR,因为我可以控制结构的创建。我把这个职责封装成一个单一的功能。我只需要改变一个地方。
每个结构都值得拥有它的构造函数!你未来的自己会在下一次重构会议上感谢你 :)
客观地说,有些结构体的默认值很特别,比如我经常使用的 sync.RWMutex
。
type Mutex struct {
state int32
sema uint32
}
type RWMutex struct {
w Mutex // held if there are pending writers
writerSem uint32 // semaphore for writers to wait for completing readers
readerSem uint32 // semaphore for readers to wait for completing writers
readerCount int32 // number of pending readers
readerWait int32 // number of departing readers
}
rwm := sync.RWMutex{}
rwm.Lock()
十分优雅。但我写 Go 已经 4 年了,我无法确定我单独使用默认值设计的单个结构。但是,我可以在过去 6 个月内链接到 3 个 PR,这些 PR 由于缺少专用构造函数而导致生产错误。提供了错误的默认值,因为 Go 编译器无法挑到它;从编译器的角度来看,该值不会丢失;它认为你没有通过它,因为你希望编译器使用默认值。
原因 2 - 你的代码可能每周更改一次,而 RWMutex 不会
Mutex 源源的最后一次提交是在 2019 年 11 月。现在是 2021 年 11 月。
不知道你是什么情况,但我项目中的最新提交是 3 小时前~。
如果你在项目中作为一个团队来优化灵活性和重构,那将是最好的。专用的构造函数可以帮助解决这个问题。
原因 3 - 导出的结构体和公共库
公开的结构体和库对变化更加敏感。上个月,由于缺少构造函数,我的代码出现了一个 bug。。。
一个Config
结构体是用 field:value 语法
初始化的,我添加了一个不会有“合理默认值”的新字段:
Config{
IsObserverEnabled: func() bool {
return false
},
IsFilteringEnabled: func() bool {
return false
},
}
我审查了整个代码库,调整了所有用法,更新了测试,然后发布新版本。
似乎一起都很正常。但当我将库导入到 3 个使用它的微服务时,我忽略了一个用法,并且错误的默认值潜入并弄乱了服务部署。如果只有一个专用的NewConfig()
构造函数,编译器会在导入库后抛出错误。我也不必更改 3 个微服务,只需更改NewConfig()
库中的一个函数即可!
原因 4 - 打破单一职责原则 (SRP)
似乎是因为缺少样板,当开发人员创建专用构造函数时,他们会用不属于那里的额外逻辑来劫持它,比如 Goroutines, disk 操作, DB 链接等。
func NewDatabase(cfg Config) (*DB, error) {
ctx, cancel := context.WithCancel(context.Background())
db := &DB{
// stuff
}
go db.connect(ctx)
return db
}
污染的构造函数使得维护、测试和使用变得具有挑战性。
A constructor should only create a new instance of the object
就是这样。几十年来一直如此,这一原则对我们很有帮助。当我在某个库上调用 NewDatabase(cfg)
时,我最不希望看到的是在幕后的 goroutine 中启动数据库网络连接。
你无法对其进行测试,甚至无法在数据库连接之前初始化结构体;这只是一种可怕的做法。此逻辑属于专用Connect()
函数,而不是构造函数。两种不同的职责。
以上就是我使用 Go 和构造函数的经验,希望对你有帮助!
原文链接:https://web3.coach/golang-why-you-should-use-constructors
往期推荐
欢迎关注「幽鬼」,像她一样做团队的核心。