点击围观!腾讯 TAPD 助力金融行业研发提效、敏捷转型最佳实践! 了解详情
写点什么

关于 Go 语言,你可能会讨厌的五件事

  • 2018-07-22
  • 本文字数:6356 字

    阅读完需:约 21 分钟

近年来,Go 从新出现的编程语言中脱颖而出。不过要把 Go 称为“新晋者”似乎并不合适,因为谷歌早在 2009 年就推出了 Go,并于 2012 年发布了第一个最终版(Go 1.0)。到现在为止,Go 已经发展到了 1.10 版本,这个版本令人印象深刻,而且还在不断添加新的特性。

为什么它被称为 eGOtistic(自大狂)……

大家都知道,Go 在实现或语法方面喜欢“我行我素”。在英语中,这种情况被描述为“自以为是”。很多来自其他编程语言的概念在 Go 中并不存在,或者即使存在,它们的行为也变得“面目全非”。后一种情况可能会导致意想不到的错误,甚至让开发人员感到疑惑。

严格的 Go 语法通常会让开发人员感到疲倦。Go 编译器不允许出现未使用的导入和变量,并竭尽所能将它们拦截下来,甚至让花括号另起一行都不行。Go 强制使用相对固定且几乎统一的编程风格。只要 Go 编译器不喜欢某些东西,到最后都变成了编译错误。

Go 提供了非常严格的类型安全。因为太过严格,我们甚至可以通过它来实现一些特殊效果和编程错误,其中一些我们稍后会在文中讨论。不过,我们很少有必要在 Go 中显式地声明类型,因为类型通常可以从赋值中获得,也就是类型推断。

我不是要提供问答!

一年多以前,我开始在工作中大量使用 Go。Go 算不上是我最喜欢的编程语言,但我承认,Go 在提升开发效率方面起到了一定作用。事实上,我已经使用 Go 完成了几个小项目,主要是一些嵌入式应用。Go Toolchain 的跨平台编译功能(编译后可用于其他操作系统或 CPU 平台)非常棒,已经遥遥领先于它的竞争对手。

现在让我们来看看 Go 的一些比较特别的特性。入门 Go 其实很容易,可能只需要一个周末来了解它的基础知识。但当你开始用 Go 做一些更复杂的事情时,各种奇奇怪怪的事件开始浮出水面。

有时候,这些特性非常奇怪,谷歌为此提供了问题解答,用于解释类似“为什么 X 的行为是这样或者那样的”这类问题。Go 在很多方面都表现得与其他语言不太一样,感觉好像程序员在某个时候一定会被某些陷阱绊倒一样。gopher Slack 频道已经证实了这种情况的存在,其中就有这样的描述:“现在你真的应该好好了解一下 Go 了,因为每个开发人员在他们的 Go 职业生涯中都会问到这个问题”。通常情况下,我们的直觉与 Go 的特性并不相符。例如,在谷歌的 C 语言变种中,公开类型、函数、常量等都以大写字母作为开头来表示它们是公开的,而标识符开头的小写字母表示它们是私有的。

尽管如此,有关 Go 的很多决策都是在邮件列表或提案文件中经过了长时间的讨论,因此还是得到了肯定。然而,讨论所使用的用例都非常特殊,以至于很多开发人员仍然不清楚这与他们要解决的问题究竟有什么关系。

我个人最喜欢的部分是 Go 没有提供可重入锁,即同一线程或 Goroutine(Coroutine 或 Green Thread 的变体)可递归获取的锁。如果不通过 hack 的方式就无法自行实现这样的功能,因为线程在 Go 中不可用,而 Goroutine 也并没有提供可用于递归识别相同 Coroutine 的标识符。

在这篇文章中,我想介绍 Go 的五个特性及其语法,这些特性都很隐晦。

1. 疯狂的影子

让我们从最简单的事情开始:每个优秀的开发人员都听说过 Shadowing,它通常会发生在变量的上下文中。下面是只包含两个作用域的简单示例:

复制代码
foo("foo")
func foo(var1 string) {
for {
var1 := "bar"
fmt.Println(var1)
break
}

我们通过:= 赋值符号创建了一个变量,并通过所赋的值(类型引用)来推断变量的类型。在这里,它是一个字符串。因此,我们在内部作用域(for 循环)中创建了一个与函数参数名称相同的变量。我们覆盖(shadow)了输入参数,并输出“bar”。

到现在为止还挺好。但是,在 Go 中,需要为其他包的属性指定包名(即结构体、方法、函数等),这个可以在提供 Println 函数的 fmt 包中看到。

所以我们对之前的例子稍微做一下重构:

复制代码
foo("foo")
func foo(var1 string) {
for {
fmt := "bar"
fmt.Println(var1)
break
}
}

这一次,我们遇到了编译错误,我们试图在一个字符串上调用 Println 函数。但这种情况并不总是这么明显。当代码突然停止编译时,即使只有几行代码也会给我们带来“惊喜”。

如果结构体发生重叠,就会很麻烦。让我们举一个奇怪的例子:

复制代码
type task struct {
}
func main() {
task := &task{}
}

我们创建了一个叫作 task 的结构体和它的一个实例。我们有意使用小写 task 作为结构体的名称,因为如前所述,Go 使用第一个字母来确定可见性,所以 task 在这里是私有的。

到目前为止,它看起来很不错,Go 编译了我们创建的 task。但是,当我们尝试添加另一行代码时,情况突然发生了变化。

复制代码
type task struct {
}
func main() {
task := &task{}
task = &task{}
}

现在无法通过编译,并显示 task 不是一个类型。此时,Go 分不清类型和变量之间的区别。也许有人会说,在 JavaScript 中,变量 task 可以是对类型的引用,但这在 Go 中是不可能的,因为类型不可以作为值赋给变量。

现在的问题是:这算不算是悲剧?一般来说不算,但它却经常在我没有意识到的情况下发生。后面可能还会有一些代码尝试访问相同名称的结构体或包,而每次都需要花几分钟时间才能找到问题所在。

说到类型问题,让我们看看另外一个例子。

2. 类型还是无类型,这是个问题!

我们已经知道如何创建结构体和函数。有时候,我们会偶尔“重命名”一下类型,比如:

type handle int这将创建一个叫作 handle 的类型,它的行为类似 int。通常,这个特性被称为类型别名。你可能也想到过这个特性,但不是在 Go 中。不过从 Go 1.9 开始,已经完全支持这个特性了。

让我们看看可以用 Go 做哪些好玩的事情:

复制代码
type handle int
func main() {
var var1 int = 1
var var2 handle = 2
types(var1)
types(var2)
}
func types(val interface{}) {
switch v := val.(type) {
case int:
fmt.Println(fmt.Sprintf("I am an int: %d", v))
case handle:
fmt.Println(fmt.Sprintf("I am an handle: %d", v))
}
}
I am an int: 1
I am an handle: 2

在这个例子中,我们使用了 Go 的几个非常酷的特性。switch-type-case 语句是一种类型模式匹配,类似于 Java 的 instanceof 或 JavaScript 的 typeof。我们把 interface{}与 Java 中的 Object 等同起来,因为它是一个空的接口,每个 Go 类都会自动实现它。

有趣的是,Java 开发人员希望 handle 也是一个 int,这样就会匹配到第一个 case。但事实并非如此,因为面向对象中的类型继承在 Go 中并不适用。

另一种可能的情况是,handle 是 int 的别名,就像 C/C++ 中的 typedef 一样,但事实也并非如此。Go 编译器会创建一个新的 TypeSpec,可以说是原始类型的克隆。因此,它们之间是完全独立的。

不过,从 Go 1.9 开始,支持真正的类型别名。下面的例子只稍微做了点修改。

复制代码
type handle = int
func main() {
var var1 int = 1
var var2 handle = 2
types(var1)
types(var2)
}
func types(val interface{}) {
switch v := val.(type) {
case int:
fmt.Println(fmt.Sprintf("I am an int: %d", v))
}
switch v := val.(type) {
case handle:
fmt.Println(fmt.Sprintf("I am an handle: %d", v))
}
}
I am an int: 1
I am an int: 2
I am an handle: 1
I am an handle: 2

你有没有注意到它们的区别?实际上,我们现在不使用 type handle int,而是使用 type handle=int 为 int 创建一个额外的名称(别名),即 handle。这意味着 switch 语句也必须做出修改,因为这个时候,int 和 handle 对于编译器来说是完全相同的类型,除非你有另一个 double case,否则会出现编译错误。由于类型别名实在 Go 1.9 中引入的,很多人会认为上述的类型克隆就是类别别名。

为了方便演示,让我们定义一个名为 Callable 的类型,它由一个没有参数和返回值的简单函数组成。

type Callable func()现在创建一个相应的函数。

复制代码
func main() {
myCallable := func() {
fmt.Println("callable")
}
test(myCallable)
}
func test(callable Callable) {
callable()
}

看,很简单。由于 Go 的类型推断机制,编译器自动识别出 myCallable 应该对应 Callable 的函数签名。编译器因此能够隐式地将 myCallable 转换为 Callable。随后,myCallable 被传递给 test 函数。这是执行隐式转换的少数例外之一,通常情况下,所有形式的转换必须全部明确地指出。

现在我们已经到了不得不使用 Reflection 的地步。与其他语言一样,Reflection 提供了在运行时分析或改变行为的能力。类型信息通常被用于根据值的数据类型来改变运行时行为。

复制代码
type Callable func()
func main() {
callable1 := func() {
fmt.Println("callable1")
}
var callable2 Callable
callable2 = func() {
fmt.Println("callable2")
}
test(callable1)
test(callable2)
}
func test(val interface{}) {
switch v := val.(type) {
case func():
v()
default:
fmt.Println("wrong type")
}
}
callable1
wrong type

callable1 现在是函数类型 func(),而 callable2 被显式声明为 Callable。 Callable 是一个单独的 TypeSpec,因此与 func() 的类型不一样。这两种情况现在都必须由我们的 Reflection 处理程序单独拦截处理。不过这些问题可以通过在 Go 1.9 中引入的类型别名来解决。

type Callable=func()### 3. 懒惰是囊地鼠的天性!

Go 语言萌萌哒的 logo 囊地鼠生性懒散,选这个 logo 也是有一定的代表意义的。

我最喜欢的 Go 特性之一是惰性求值(Lazy Evaluation),即延迟执行代码。自从 Java 推出 Stream API 以来,Java 开发人员对该特性也所了解。

我们来看看下面的代码片段:

复制代码
func main() {
functions := make([]func(), 3)
for i := 0; i < 3; i++ {
functions[i] = func() {
fmt.Println(fmt.Sprintf("iterator value: %d", i))
}
}
functions[0]()
functions[1]()
functions[2]()
}

这里有一个包含三个元素的数组、一个循环和闭包,而结果会是什么?

复制代码
iterator value: 3
iterator value: 3
iterator value: 3

我们会认为是 0,1,2,但实际上却是 3,3,3。没错!

在其他编程语言(如 Java)中,在创建闭包时会捕获变量的值,而 Go 仅捕获指向变量本身的指针。问题是,在迭代期间,变量的值不断变化。循环完成后,我们执行闭包,只看到最后的值。我们知道我们只拥有指针,所以也就可以理解这种行为,但确实不是很直观。

如果我们想保存这个值,需要知道在创建闭包时如何计算这个值。

复制代码
func main() {
functions := make([]func(), 3)
for i := 0; i < 3; i++ {
functions[i] = func(y int) func() {
return func() {
fmt.Println(fmt.Sprintf("iterator value: %d", y))
}
}(i)
}
functions[0]()
functions[1]()
functions[2]()
}

我们创建了一个临时函数,它将变量作为参数并返回闭包。我们立即调用这个函数。由于在调用外部函数时必须先计算变量的值,所以内部闭包就可以捕获到正确的值。我们得到的是 0,1,2。

在写这篇文章不久之前,我找到了另一种方式。我们可以在循环中创建一个具有相同名称的变量,并为其分配实际值。这样也可以捕获到变量的值,因为这个方法在循环的每次迭代中都会创建一个新的变量(因此是一个新的指针)。

复制代码
func main() {
functions := make([]func(), 3)
for i := 0; i < 3; i++ {
i := i // Trick mit neuer Variable
functions[i] = func() {
fmt.Println(fmt.Sprintf("iterator value: %d", i))
}
}
functions[0]()
functions[1]()
functions[2]()
}

从执行速度来看,懒求值通常是一个有趣的话题。毕竟,我可以在不使用它的情况下创建闭包。既然这样,为什么还要求值?在我看来,这也是非常不直观的。

4. 我们是不是都有点像囊地鼠?

我们已经知道,Go 中的 interface{}就像 Java 中的 Object——Go 中的每个类型都会自动实现这个空接口。不过,自动实现接口不仅适用于空接口,每一个实现了某个接口所有方法的结构体或类型也会自动实现这个接口。

为了更好地说明这个问题,让我们来看看下面的例子:

复制代码
type Sortable interface {
Sort(other Sortable)
}

定义了这个方法的结构体会自动成为 Sortable。

复制代码
type MyStruct struct{}
func (m MyStruct) Sort(other Sortable){}

除了接收器类型的语法,它用于将函数绑定到类型(在本例中为结构体),我们已经实现了 Sortable 接口的所有方法。我们现在是一个 Sortable!

var sortable Sortable = &MyStruct{}自动实现接口乍一看似乎很有用,但这样会让事情变得复杂,特别是在大型应用中,如果有几个接口拥有相同的方法,那么就会点让人摸不着头脑。开发者实际想要实现哪个接口?或许他们应该在代码的注释中写清楚!

Go 还有一个解决方案用于确保一个类型实现了一个接口,就像 Java 的 implements 关键字一样,这实在是太简单了。

复制代码
type MyStruct struct{}
func (m MyStruct) Sort(other Sortable){}
var _ Sortable = MyStruct{}
var _ Sortable = (*MyStruct)(nil)

5.nil 和 nothing

现在我们都知道,“null”和“nil”之间有很大的差别,但可能不是所有人都知道,“nothing”并不总是意味着“什么都没有”。为了证明这点,我们定义了自己的错误类型(异常)。

复制代码
type MyError string
func (m MyError) Error() string {
return string(m)
}

我们创建了一个新的类型,它是从字符串类型克隆过来的。我们只是想要一个错误消息,所以这样做就足够了。要实现 error 接口(是的,小写,理论上它不应该是公开的,但 Go 无所不能),就必须实现 Error 方法。

接下来,我们需要另一个总是返回 Nil 的函数。

复制代码
func test(v bool) error {
var e *MyError = nil
if v {
return nil
}
return e
}

无论我们传进去的是 true 还是 false,这个函数总是返回 nil,是这样的吗?

复制代码
func main() {
fmt.Println(nil == test(true))
fmt.Println(nil == test(false))
}
true
false

在返回 e 时,*MyError 指针指向接口 error 的一个实例,它不是 nil!这样合逻辑吗?当你知道接口在 Go 中的表示方式,你就会知道这是合乎逻辑的。

在 Go 内部,接口是一个结构体,包含了实际目标实例(这里为 nil)和接口类型(在这里是 error),而且根据 Go 语言规范,只有在这个结构体的两个值都为 nil 时,接口实例才为 nil。因此,如果真想要返回 nil,那就显式地返回吧。

特别之处

还有一点是值得一提的,如前所述,Go 根据名称来推断出类型和功能的可见性。如果第一个字母是大写字母(如 Foo),则该函数或类型是公开的,如果第一个字母是小写字母(如 foo),那么就是私有的。不过,在 Java 中有 private,而在 Go 中只有 package-private。

一般来说,除了在 Go 中使用驼峰式命名法,我们都可以使用这种可见性规则,无论是函数、结构体还是常量,但我们的 IDE 有语法突出显示,所以谁会在乎这个!

有趣的是,Go 支持 Unicode 的标识符。因此,日本语(Nihongo 是日语的意思)是完全合法的标识符,但通常被认为是私有的。为什么?因为日文字符没有大写字母。

“GO 斯拉”发来问候

某种程度上,Go 是一门非常独特的语言。在日常工作中,你可以享受 Go 带来的乐趣。如果你已经知道我们在这里所提到的陷阱(还有更多),那么即使开发再大型的应用程序也不成问题。尽管如此,还是会不断出现各种提醒,说这门语言有问题。

Go 在近几年发生了很多事情,除了增加新特性,Go 2 中还列出了很多需要改进的地方,包括一些语法和运行时行为的不一致性。不过 Go 2 的推出时间还不得而知,还没有清晰的路线图。

如果你想要用 Go,那么就用吧,尽管存在很多坑。不过你要为此做好准备:有时候你会感到困惑,需要长时间的调试,或通过阅读 FAQ 或访问 Gopher Slack 频道来解决问题。

原文链接: https://jaxenter.com/5-things-you-hate-about-go-143422.html

感谢张婵对本文的审校。

2018-07-22 12:405293
用户头像

发布了 731 篇内容, 共 416.6 次阅读, 收获喜欢 1979 次。

关注

评论

发布
暂无评论
发现更多内容

英特尔:i7-10870H 游戏性能超 R7 5800H,更强的 11 代酷睿 H 在后面

新闻科技资讯

微服务指南

码语者

DevOps

当AI开始改造“文房四宝”:腾讯教育的脑洞与逻辑

脑极体

一个简单实用的Linux性能分析工具

运维研习社

Linux 性能分析

Nginx 模块系统:前篇

soulteary

nginx 动态模块

金三银四如何突击面试美团?面试题(含答案)+学习笔记+电子书籍+学习视频

比伯

Java 编程 架构 面试 程序人生

CentOS安装Docker运行环境

wjchenge

Docker Centos 7

CodeHub#4 启动报名| 荷小鱼:K12 在线教育应用的开发实践

蚂蚁集团移动开发平台 mPaaS

在线教育 mPaaS codehub 离线包

是什么支持“毅力号”在火星上尽情摄影?

亚马逊云科技 (Amazon Web Services)

力扣(LeetCode)刷题,简单+中等题(第32期)

不脱发的程序猿

算法 LeetCode 编程能力 28天写作 3月日更

中国程序员最容易发错的单词

happlyfox

GitHub 学习 程序人生 3月日更

华山版强势来袭!阿里巴巴Java性能优化2021年3月版(面试必备)

Java架构追梦

Java 阿里巴巴 架构 面试 性能优化

如何解决移动直播下的耳返延迟问题

融云 RongCloud

音视频 移动直播

酷睿i7-10870H对比锐龙7 5800H游戏性能, 英特尔仍是游戏本CPU的更优选

新闻科技资讯

大赛报名|首次聚焦口罩场景!第三届 106 点关键点定位大赛开启

京东科技开发者

人工智能 深度学习 计算机视觉

重磅!Flutter中网络图片加载和缓存源码分析,BAT大厂面试总结

欢喜学安卓

android 程序员 面试 移动开发

2021 创新加速周蓄势待发,铆足牛劲再出发!

亚马逊云科技 (Amazon Web Services)

工作中,有哪些SQL是我们必须要掌握的?

xiezhr

oracle sql SQL语法 3月日更

【LeetCode】用栈实现队列Java题解

Albert

算法 LeetCode 28天写作

农田治理效率低下还赔本?智慧农业力保粮食品质,效率事半功倍

一只数据鲸鱼

物联网 数据可视化 智慧城市 智慧农业 农业管理

Pano React Native SDK 来了!快速实现移动端音视频和白板

拍乐云Pano

flutter ios android RTC React Native

人民网:亚马逊云科技,以这样姿势扎根中国!

亚马逊云科技 (Amazon Web Services)

建信金科大咖访谈:金融科技驱动业务创新,智慧运营引领发展转型

金科优源汇

百亿级流量的百度搜索中台,是怎么做可观测性建设的?

百度Geek说

中台 云原生 #百度#

腾讯T2大牛手把手教你!2021新一波程序员跳槽季,算法太TM重要了

欢喜学安卓

android 程序员 面试 移动开发

【得物技术】会议室巡检系统(哮天犬)部署分享

得物技术

分享 部署 巡检 得物技术 会议室

程序员之禅(三)

每天读本书

每天读本书

SQL Server 删除正在使用数据库

田镇珲

报名 | 全球首个小资源音色克隆赛结果出炉,高分队伍线上报告会

爱奇艺技术产品团队

萌新不看会后悔的C++基本类型总结(一)

花狗Fdog

企业级链表设计思路:

大忽悠

3月日更

关于Go语言,你可能会讨厌的五件事_语言 & 开发_Christoph Engelbert_InfoQ精选文章