为何Go是一种设计糟糕的编程语言

好吧,我认可这个标题有点放肆。我多告诉你一点:我爱肆意妄言的标题,它可以吸引注意力。无论怎样,在这篇博文中我会试图证实 Go 是一个设计得很糟糕的语言(剧透:事实上它是)。我已经摆弄 Go 有几个月了,并且,我想我在六月某个时候运行了第一个 helloworld 程序。虽然个人数学不太好,但在那以后已经有四个月了,而且个人 Github 上已经有了几个 package。没必要多说,我仍彻底没有在生产中使用 Go 的经验,因此把我说的有关 “编码支持”、“部署”以及相关内容看成不可尽信的吧。html

我喜欢 Go语言。自从试用了它之后我就爱上了。我花了几天来接受 Go 的语言习惯,来克服没有泛型的困难,了解奇怪的错误处理和 Go 的全部典型问题。我读了 Effective Go,以及 Dave Cheney 的博客上的许多文章,并且注意与 Go 有关的一切动向等等。我能够说我是一个活跃的社区成员!我爱 Go 并且我没法自拔—Go 使人惊奇。然而依我拙见,与它所宣传的正好相反,Go 是一个设计糟糕、劣质的语言。git

Go 被认为是一个简练的编程语言。根据 Rob Pike 所说,他们使出了浑身解数来使这个语言的规范简单明了。这门语言的这一方面是使人惊奇的:你能够在几小时内学会基础而且直接开始编写能运行的代码,大多数状况下 Go 会如所期待的那样工做。你会被激怒,可是但愿它管用。现实并不同,Go语言并非一个简洁,它只是低劣。如下有一些论点来证实。程序员

理由1. 切片(Slice)操做压根就不对!

切片很棒,我真的很喜欢这个概念和一些用法。可是让咱们花一秒钟,想象一下咱们真的想要去用切片写一些代码。显而易见,切片存在于这门语言的灵魂中,它让 Go 强大。可是,再一次,在“理论”讨论的间隙,让咱们想象一下咱们有时会写一些实实在在的代码。如下列出的代码展现了你在 Go 中如何作列表操做。github

// 请给我一些数字!
numbers := []int{1, 2, 3, 4, 5}

log(numbers)         // 1\. [1 2 3 4 5]
log(numbers[2:])     // 2\. [3 4 5]
log(numbers[1:3])    // 3\. [2 3]

// 有趣的是,你不能使用负数索引
//
// 来自 Python 的 numbers[:-1] 并不能正确工做,相反的是,
// 你必须这样作:
//
log(numbers[:len(numbers)-1])    // 4\. [1 2 3 4]

// 可读性真实“太好了”,Pike 先生!干的漂亮!
//
// 如今,让咱们在尾部插入一个6:
//
numbers = append(numbers, 6)

log(numbers) // 5\. [1 2 3 4 5 6]

// 把3从numbers中移除 :
//
numbers = append(numbers[:2], numbers[3:]...)

log(numbers)    // 6\. [1 2 4 5 6]

// 想要插入一些数?别急,这里是一个Go语言*通用*最佳实践
//
// 我特别喜欢。。。哈哈哈。
//
numbers = append(numbers[:2], append([]int{3}, numbers[2:]...)...)

log(numbers)    // 7\. [1 2 3 4 5 6]

// 为了拷贝一份切片,你须要这样作:
//
copiedNumbers := make([]int, len(numbers))
copy(copiedNumbers, numbers)

log(copiedNumbers)    // 8\. [1 2 3 4 5 6]

//还有一些其余操做。。。
复制代码

信不信由你,这是 Go 程序员天天如何转换切片的真实写照。并且咱们没有任何泛型机制,因此,哥们,你不能创造一个漂亮的 insert() 函数来掩盖这个痛苦。我在 playgroud 贴了这个,因此你不该该相信我:本身双击一下去亲自看看。golang

理由2. Nil 接口并不老是 nil 🙂

他们告诉咱们“在 Go 中错误不仅是字符串”,而且你不应把它们当字符串对待。好比,来自 Docker 的 spf13 在他精彩的“Go 中的7个失误以及如何避免”中如此讲过。编程

他们也说我应该老是返回 error 接口类型(为了一致性、可读性等等)。我在如下所列代码中就是这么作的。你会感到惊讶,可是这个程序真的会跟 Pike 先生 say hello,可是这是所期待的吗?安全

package main

import "fmt"

type MagicError struct{}

func (MagicError) Error() string {
	return "[Magic]"
}

func Generate() *MagicError {
	return nil
}

func Test() error {
	return Generate()
}

func main() {
	if Test() != nil {
		fmt.Println("Hello, Mr. Pike!")
	}
}
复制代码

是的,我知道为何这会发生,由于我阅读了一堆复杂的关于接口和接口在 Go 中如何工做的资料。可是对于一个新手……拜托哥们,这是当头一棒!实际上,这是一个常见的陷阱。如你所见,没有这些让人心烦意乱的特性的 Go 是一个直接易学的语言,它偶尔说 nil 接口并非nil 😉markdown

理由3. 好笑的变量覆盖

为了以防万一你对这个术语不熟悉,让我引用一下 Wikipedia:”当在某个做用域(断定块、方法或者内部类)中声明的一个变量与做用域外的一个变量有相同的名字,变量覆盖就会发生。“看上去挺合理,一个至关广泛的作法是,多数的语言支持变量覆盖并且这没有问题。Go 并非例外,可是却不太同样。下面是覆盖如何工做的:app

package main

import "fmt"

func Secret() (int, error) {
	return 42, nil
}

func main() {
	number := 0

	fmt.Println("before", number) // 0

	{
		// meet the shadowing
		number, err := Secret()
		if err != nil {
			panic(err)
		}

		fmt.Println("inside", number) // 42
	}

	fmt.Println("after", number) // 0
}
复制代码

是的,我也认识到 := 操做符制造了一个新的变量而且赋了一个右值,因此根据语言规范这是一个彻底合法的行为。可是这里有件有意思的事:试着去掉内部做用域——它会如指望的运行(”在42以后“)。不然,就跟变量覆盖问个好吧。编程语言

无需赘言,这不是什么我在午餐时想起来的一个好玩的例子,它是人们迟早会遇到的真实的东西。这周的早些时候我重构了一些 Go 代码,就遇到了整个问题两次。编译没问题,代码检查没问题,什么都没问题——代码就是不正常运行。

理由4. 你不能传递把 []struct 做为 []interface 传递

接口很棒,Pike&Co. 一直说它就是 Go 语言的一切:接口事关你如何处理泛型,如何作 mock 测试,它是多态的实现方法。让我告诉你吧,当我阅读“Effective Go”的时候我真心爱着接口,并且我一直爱着它。除了上面我提出的“nil 接口不是 nil”的问题外,这里有另外一个使人讨厌的事让我认为接口在 Go 语言中没有获得头等支持。基本上,你不能传递一个结构的切片到一个接收接口类型切片的函数上:

package main

import (
	"fmt"
	"strconv"
)

type FancyInt int

func (x FancyInt) String() string {
	return strconv.Itoa(int(x))
}

type FancyRune rune

func (x FancyRune) String() string {
	return string(x)
}

// 实际上,任何具备String()方法的对象
type Stringy interface {
	String() string
}

// String, made of string representations of items given.
func Join(items []Stringy) (joined string) {
	for _, item := range items {
		joined += item.String()
	}

	return
}

func main() {
	numbers := []FancyInt{1, 2, 3, 4, 5}
	runes := []FancyRune{'a', 'b', 'c'}

	// You can't do this!
	//
	// fmt.Println(Join(numbers))
	// fmt.Println(Join(runes))
	//
	// prog.go:40: cannot use numbers (type []FancyInt) as type []Stringy in argument to Join
	// prog.go:41: cannot use runes (type []FancyRune) as type []Stringy in argument to Join
	//
	// 相反,你应该这样作:
	//

	properNumbers := make([]Stringy, len(numbers))
	for i, number := range numbers {
		properNumbers[i] = number
	}

	properRunes := make([]Stringy, len(runes))
	for i, r := range runes {
		properRunes[i] = r
	}

	fmt.Println(Join(properNumbers))
	fmt.Println(Join(properRunes))
}
复制代码

不出意外,这是个已知的根本没有被看成问题的问题。它只是 Go 的又一个好笑的事,对吧?我真的推荐你阅读一下相关的 wiki,你会发现为何“传递结构切片做为借口切片”不可行。可是呀,好好想一想!咱们能够作到,这里没什么魔法,这只是编译器的问题。看,在 49-57行 我作了一个由 []struct 到 []interface的显式转换。为何 Go 编译器不为我作这些?是的显示要比隐式好,可是WTF?

我只是没法忍受人们看着这种狗屁语言又一直说“好,挺好的”。并非。这些让 Go 变成了一个糟糕的语言。

理由5. 不起眼的 range“按值”循环

这是我曾经遇到过的第一个语言问题。好吧,在 Go 中有一个 “for-range”循环,是用来遍历切片和监听 channel 的。它处处都用获得并且还不错。然而这里有一个小问题,大多数新手被坑在这上面:range 循环只是按值的,它只是值拷贝,你不能真的去作什么,它不是 C++ 中的 foreach。

package main

import "fmt"

func main() {
	numbers := []int{0, 1, 2, 3, 4}

	for _, number := range numbers {
		number++
	}

	fmt.Println(numbers) // [0 1 2 3 4]

	for i, _ := range numbers {
		numbers[i]++
	}

	fmt.Println(numbers) // [1 2 3 4 5]
}
复制代码

请注意,我没有抱怨 Go 里没有按引用的 range,我抱怨的是 range 太不起眼。动词“range”有点像是说“遍历项目“,而不是”遍历项目的拷贝“。让咱们看一眼”Effective Go“中的 For,它听起来一点也不像”遍历切片中的拷贝值“,一点也不。我赞成这是个小问题,我很快(几分钟)就克服了它,可是没有经验的 gopher 也许会花上一些时间调试代码,惊讶于为何值没有改变。大家至少能够在”Effective Go“里面把这点讲述明白。

理由6. 可疑的编译器严谨性

就如我以前已经告诉你的,Go被认为是一个有着严谨的编译器的,简单明了而且可读性高的语言。好比,你不能编译一个带有未使用的 import 的程序。为何?只是由于 Pike 先生认为这是对的。信不信由你,未使用的 import 不是世界末日,我彻底能够与其共存。我彻底赞成它不对并且编译器不惜打印出相关的警告,可是为何你为了这么一个小事停止编译?就为了未使用的 import,当真?

Go1.5 引入了一个有趣的语言变化:如今你能够列出 map 字面量,而没必要显示列出被包含的类型名。这花了他们五年(甚至更多)来认识到显示类型列出被滥用了。

另外一个我在 Go 语言里很是享受的事情:逗号。你看,在 Go 中你能够自由地定义多行 import、const 或者 var 代码块:

import (
    "fmt"
    "math"
    "github.com/some_guy/fancy"
)
const (
    One int = iota
    Two
    Three
)
var (
    VarName int = 35
)
复制代码

好吧,这挺好的。可是一旦它涉及到“可读性”,Rob Pike 认为加上逗号会很棒。某一刻,在加上逗号之后,他决定你应该也把结尾的逗号留着!因此你并不这样写:

numbers := []Object{
    Object{"bla bla", 42}
    Object("hahauha", 69}
}
复制代码

你必须这样写:

numbers := []Object{
    Object{"bla bla", 42},
    Object("hahauha", 69},
}
复制代码

我仍然怀疑为何咱们在 import/var/consts 代码块中能够忽略逗号,可是在列表和映射中不能。不管如何,Rob Pike 比我清楚!可读性万岁!

理由7. Go generate 太诡异了

首先,你要知道我没有反对代码生成。对于 Go 这样一个粗劣的语言,这也许是仅有的可用来避免拷贝-粘贴一些常见的东西的途径。然而,Go:generate——一个 Go 用户处处都用的代码生成工具,如今仅仅是垃圾而已。好吧,公平来讲,这个工具自己还好,我喜欢它。而整个的方式是错的。咱们看看吧,你要经过使用特别的魔法命令来生成一些代码。对,经过代码注释中的一些神奇的字节序列来作代码生成。

注释是用来解释代码,而不是生成代码。不过神奇的注释在当今的 Go 中是一种现象了。很是有意思的是,没人在意,你们以为这就挺好的。依我愚见,这绝对比吓人的未使用的 import 要糟糕。

后记

如你所见,我没有抱怨泛型、错误处理、语法糖和其余 Go 相关的典型问题。我赞成泛型不相当重要,但若是你去掉泛型,请给咱们一些正常的代码生成工具而不是随机的乱七八糟的狗屎神奇注释。若是你去掉异常,请给咱们安全地把接口与 nil 比较的能力。若是你去掉语法糖,请给咱们一些可以如预期工做的代码,而不是一些像变量遮蔽这样的“哎呦卧槽“的东西。

总而言之,我会继续使用 Go。理由以下:由于我爱它。我恨它由于它就是堆垃圾,可是我爱它的社区,我爱它的工具,我爱巧妙的设计决定(接口你好)和整个生态。

嘿伙计,想尝试尝试 Go 吗?

相关文章
相关标签/搜索