最近为了看k8s代码,我开始复习早已被忘记的golang。上一次接触golang还是上学的时候,仅仅是单纯的记忆一下语法规则,配置一下本地环境,跑几个简单的示例程序,并没有想太多深层次的“why”。
经过这两年的工作,多问“为什么”已经成为我一个习惯性的学习方法。在目前为止复习golang的过程中,我注意到golang有一些相对特别的设计:
- golang中没有函数内静态成员变量
- golang中没有结构体内静态成员变量
- golang中没有结构体的构造函数,无法直接在定义时给成员变量赋值
为什么golang采用了以上的设计呢?经过阅读与思考后我理解如下。
对于第一点“golang中没有函数内静态成员变量”,一般的解决方法是使用闭包得到得与静态变量的相同作用的变量:
func getFunction(initValue1 initType1, ...) func (Type1, ...) (returnType1. ...){
return function(Type1, ...){
// initValue1... 在返回的函数中就相当于一个静态成员变量
}
}
这种方法比起原先使用静态成员变量的方法有一下几个好处:
- 函数
getFunction
实际上相当于一个工厂模式,可以返回多个“静态成员变量”初值不同的函数供使用,而不是在文件中定义一个这种函数。 - 为了得到返回的函数,必须向
getFunction
提供参数,这相当于强制要求动态提供“静态成员变量”的值,这样很大程度上防止了在代码源文件中写一个magic value给initValue1
。 - 最后一个好处微不足道,那就是可以节省一点点空间。真正的静态成员变量类似于全局变量,会在整个程序的生存期一直存在于内存中的。但闭包函数中使用的外部变量(
initValue1
)和闭包函数本身的生存期是一样的,所以闭包函数不可达被GC时,golang中所谓的“静态成员变量”的内存空间是会被回收的。
对于第二点“golang中没有结构体内静态成员变量”,一般的解决方法是在结构体所在的package(包)中定义全局变量,这样就可以所有结构体共享:
package myStructPackage
var staticField0 type0 // 实际上称之为global field也是可以的
type myStruct struct {
filed1 type1
filed2 type2
}
这样实际上:
- 变相劝说程序员不要在定义了结构体的包中包含与结构体本身不相关代码,否则全局变量可能被搞混。
- 可能还有一个原因是上面这种使用方法已经和“静态成员变量”非常接近了,设计者觉得不值得为了这样一点额外的功能增加关键字,同时弱化package在管理全局变量上的作用。
- 最后一点是golang的主要设计目标是方便多线程高并发程序的编写,方便软件使用不断提升的多核CPU等硬件的性能,而多线程中最好避免共享变量的出现(读写竞争,CPU某个核心内全局变量cache失效,容易写出bug等)。于是golang干脆取消static,引导大家少用共享变量。
对于第三点,“golang中没有结构体的构造函数,无法直接在定义时给成员变量赋值”我认为原因有三个:
- 是golang去掉函数重载后必须做出的决定,否则只允许一种构造函数对于程序的编写和阅读都是一种障碍。那么为什么golang不支持函数重载呢?据说这是为了防止值的隐式转换(这是golang的原则之一)。
- golang不支持继承,那么对于继承来说必须的构造函数在golang中没有必须存在的理由。
- 一定程度上提醒程序员避免在结构体中通过直接赋值的方式引入固定的magic value。当然,如果在自定义的NewStruct工厂类方法中仍然使用固定的magic value,那也是无法避免的。但是至少golang已经在语法层面上将“方便但不好”的写法的门槛提高了一点。
最后,至于golang这些设计有什么坏处吗?当然是有的,任何事物都有两面性。我认为这些设计最大的问题是降低了程序质量(可读性,可重构性)的下限。一个golang的新手有更大概率写出比使用C++/Java更令人费解和容易出错的代码。在工程中,golang对程序员的个人水平和团队进行code review的提出了更高要求。