爲何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 嗎?

相關文章
相關標籤/搜索