Golang 1.13 優(yōu)缺點(diǎn)整理

Go 1.14更新:

Module support in the go command is now ready for production use.
We encourage all users to migrate to go modules for dependency management.

好消息呀!Go module終于被官方推薦使用在生產(chǎn)中了。官方也建議將項(xiàng)目遷移到Go module下管理。Go被詬病了多年的包管理問(wèn)題終于解決了(似乎?)。

但是泛型功能什么時(shí)候才能來(lái)呢??。。

Go語(yǔ)言已經(jīng)被很多大型企業(yè)使用在其基礎(chǔ)設(shè)施的開(kāi)發(fā)了。作為一個(gè)新生語(yǔ)言,為什么它有這么大的魅力呢?下面我整理了網(wǎng)上各位大佬的觀點(diǎn),配合我自己對(duì)Go的理解,來(lái)總體介紹一下這門(mén)語(yǔ)言的優(yōu)缺點(diǎn)(主要以C/C++、Java、Python作為比較對(duì)象)。

如果你是C/C++、Java、Python的開(kāi)發(fā)者,可以完全看懂這篇文章。快來(lái)和我一切速覽Go語(yǔ)言吧!

我這里是以go 1.13作為基準(zhǔn)的。

本文不是Go語(yǔ)言教程,不介紹語(yǔ)言細(xì)節(jié),只是粗略地展示一下Go語(yǔ)言。

本文將會(huì)持續(xù)更新~

下面的目錄供你先預(yù)覽:

Go優(yōu)點(diǎn)

開(kāi)發(fā)效率

在寫(xiě)Go的時(shí)候,你會(huì)很容易寫(xiě)出動(dòng)態(tài)類型語(yǔ)言的感覺(jué)。這是一個(gè)非常好的特性,得益于Go的自動(dòng)類型推斷(類似于C++11的auto),大多數(shù)情況下,你可以不用關(guān)心變量的具體類型。

如果你是從Python來(lái)的程序員,會(huì)很喜歡這一點(diǎn)。

雖然Go的寫(xiě)法很有動(dòng)態(tài)類型語(yǔ)言的感覺(jué),但是它實(shí)際上是一個(gè)靜態(tài)類型語(yǔ)言,這樣就不會(huì)有動(dòng)態(tài)語(yǔ)言的缺點(diǎn),能在編譯時(shí)檢查出很多問(wèn)題(動(dòng)態(tài)語(yǔ)言只能在運(yùn)行時(shí)檢查出來(lái))。

下面我們簡(jiǎn)單對(duì)比一下Go和Java聲明變量的不同:

java:

// 簡(jiǎn)單變量
Integer a = 23;
// 對(duì)象
Person person = new Person();
// 通過(guò)方法獲取
Something sth = createSomething();

go:

// 簡(jiǎn)單變量
a := 23
// 對(duì)象
person := Person{}
// 通過(guò)方法獲取
sth := CreateSomething()

Go是不是很有Python的感覺(jué)呢?

運(yùn)行效率

Go語(yǔ)言的運(yùn)行效率是很高的。目前Go的運(yùn)行效率和Java差不多,但是Go比優(yōu)化了多年的Java年輕的多,因此潛力也更多。

在高并發(fā)的情況下,Go的表現(xiàn)會(huì)更好。因此很多企業(yè)將Go作為服務(wù)器語(yǔ)言,用于替換原先C++的位置。

雖然Go比C++還是要慢的,但是它的開(kāi)發(fā)效率比C++實(shí)在是高上太多了,在硬件越來(lái)越便宜的今天,Go未來(lái)在服務(wù)器基礎(chǔ)設(shè)施領(lǐng)域必定會(huì)占據(jù)更多市場(chǎng)。

少即是多(缺點(diǎn)?)

Go語(yǔ)言遵循“少即是多”的設(shè)計(jì)理念,提供更少的語(yǔ)言特性。這會(huì)讓Go語(yǔ)言顯得不那么“臃腫”。特別是OOP,Go語(yǔ)言在OOP上更像C語(yǔ)言,將OOP神秘的面紗揭示得一干二凈。在Go中,僅提供了結(jié)構(gòu)體、組合等少數(shù)幾個(gè)功能,沒(méi)有直接提供繼承等功能,OOP只是一個(gè)語(yǔ)法糖。

甚至對(duì)于封裝,Go語(yǔ)言也只是一個(gè)命名的事而已(首字母大寫(xiě)即為public,小寫(xiě)為private)。

下面我們用Java和Go進(jìn)行一個(gè)簡(jiǎn)單的對(duì)比:

java:

public class Student {
    private String name;
    private int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void show() {
        System.out.println(this.name, this.age)
    }
}

go:

type Student struct {
    name string
    age  int
}

func NewStudent(name string, age int) Student {
    return Student{name: name, age: age}
}

func (student Student) Show() {
    fmt.Println(student.name, student.age)
}

這種一切從簡(jiǎn)的設(shè)計(jì)思路很受很多C語(yǔ)言程序員的喜愛(ài),但是也有一些從高級(jí)語(yǔ)言轉(zhuǎn)過(guò)來(lái)的人認(rèn)為,Go提供的語(yǔ)言特性太少了;當(dāng)然也有人覺(jué)得這種特性少的語(yǔ)言更能剖析編程的本質(zhì),寫(xiě)出來(lái)的代碼更加有美感。

當(dāng)然,給小白的好處就是,Go學(xué)習(xí)起來(lái)比其他語(yǔ)言簡(jiǎn)單,特別是對(duì)于C/C++程序員來(lái)說(shuō),轉(zhuǎn)型做Go是很簡(jiǎn)單的。

當(dāng)然,關(guān)于“少即是多”是好是壞,就仁者見(jiàn)仁智者見(jiàn)智了。

gofmt格式統(tǒng)一

go語(yǔ)言提供了很多小工具,其中最受歡迎的是gofmt。gofmt可以把任意格式的Go語(yǔ)言源代碼統(tǒng)一格式化為統(tǒng)一的格式。

這樣,有了官方指定的格式,我們終于不用為了代碼格式的統(tǒng)一吵得焦頭爛額了。直接gofmt一下即可。

并發(fā)

這是Go的一大賣點(diǎn),Go是原生支持并發(fā)的,并且Go的并發(fā)單位是協(xié)程(在Go中被叫做goroutine)。

關(guān)于協(xié)程的介紹,請(qǐng)見(jiàn):Go協(xié)程。

在go中,只需要一個(gè)go關(guān)鍵字就可以實(shí)現(xiàn)啟動(dòng)一個(gè)協(xié)程并運(yùn)行。這是一個(gè)非常棒的特性,對(duì)于其它大部分語(yǔ)言,都需要使用系統(tǒng)庫(kù)來(lái)實(shí)現(xiàn)并發(fā)。

下面再來(lái)對(duì)比Java和Go:

java:

package com.test.main;

import java.lang.Runnable;
import java.lang.Thread;

class Worker implements Runnable {
    @Overried
    public void run() {
        System.out.println("run concurrent");
    }
}

public class Main {
    public static void main(String[] args) {
        Worker worker = new Worker();
        new Thread(worker).start()
    }
}

go:

package main

import "fmt"

func main() {
    go func() {
        fmt.Println("run concurrent")
    }()
}

怎么樣,是不是寫(xiě)起來(lái)比Java簡(jiǎn)單多了。

Java的juc包支持很多并發(fā)控制的工具,例如Executor、LockCountDownLatch、CyclicBarrier、Semaphore、Exchanger等。這些工具需要花一定時(shí)間去學(xué)習(xí)。

但是在Go中,goroutines之間的交互更多是用channel來(lái)實(shí)現(xiàn),當(dāng)然,Go也有提供Lock、WaitGroup(類似于Java的CountDownLatch)等功能。但是channel卻可以完成goroutine之間同步的大部分需求(Go開(kāi)發(fā)者也建議多使用channel)。這也符合Go的少即是多設(shè)計(jì)理念呢。

而且,channel也是Go內(nèi)置支持的呢。

部署簡(jiǎn)單

Go的build輸出的直接是可以運(yùn)行的二進(jìn)制文件,這就比Java簡(jiǎn)單多了。這意味我們的部署只需要簡(jiǎn)單地把一個(gè)二進(jìn)制文件丟到服務(wù)器上運(yùn)行即可(甚至服務(wù)器不需要安裝Go環(huán)境),而如果是Java,還需要在服務(wù)器上安裝一個(gè)jre。這對(duì)運(yùn)維部署人員來(lái)說(shuō)是個(gè)好事。

Go的部署也比C/C++簡(jiǎn)單,Go不需要什么繁瑣的靜態(tài)鏈接、動(dòng)態(tài)鏈接的過(guò)程。所有代碼倉(cāng)庫(kù)都會(huì)被編譯到一個(gè)可執(zhí)行文件中(當(dāng)然,這也有可能導(dǎo)致Go的可執(zhí)行文件比較大)

在大多數(shù)時(shí)候,只需要下面一行命令即可編譯Go項(xiàng)目為可執(zhí)行文件:

$ go build -o runnable main.go

go構(gòu)建器會(huì)自動(dòng)解決所有庫(kù)、連接等問(wèn)題。我們不用再去寫(xiě)冗長(zhǎng)的makefile了,也不需要專門(mén)去搞個(gè)maven這樣的第三方構(gòu)建工具。

庫(kù)

Go自帶的庫(kù)特別強(qiáng)大,特別是它的http庫(kù),可以滿足大多數(shù)Web開(kāi)發(fā)的需求了。更不用說(shuō)它的json、net、text、runtime等庫(kù)了。

自帶map

Go語(yǔ)言是原生支持map的。在Java中要使用map需要進(jìn)行導(dǎo)包,C++更不用說(shuō),光stlboost的選擇就夠你頭痛的了。

下面我們還是拿Java和Go做個(gè)對(duì)比:

java:

Map<String, Object> man = new HashMap<>();

man.put("name", "Wang");
man.put("age", 18);
man.put("birthday", new Date());

System.out.println("name =", man.get("name"));
System.out.println("age =", man.get("age"));
System.out.println("birthday =", man.get("birthday"));

go:

man := make(map[string]interface{})

man["name"] = "Wang"
man["age"] = 18
man["birthday"] = time.Now()

fmt.Println("name =", man["name"])
fmt.Println("age =", man["age"])
fmt.Println("birthday =", man["birthday"])

這樣的寫(xiě)法更像Python。

生態(tài)圈

Go有一個(gè)殺手級(jí)別的項(xiàng)目:Docker。以及Kubernetes。這兩個(gè)東西的火熱程度不用說(shuō)大家心理已經(jīng)知道。

以容器技術(shù)在未來(lái)的趨勢(shì),只要Docker不倒,Go在容器領(lǐng)域就會(huì)一直火熱下去。

測(cè)試

Go自帶了一套很好用的測(cè)試組件,Go的測(cè)試不再使用傳統(tǒng)的assert。測(cè)試失敗需要手動(dòng)調(diào)用函數(shù)。

在測(cè)試失敗后,Go也不會(huì)馬上終止測(cè)試程序,而是會(huì)把測(cè)試程序堅(jiān)持運(yùn)行完畢。

Go的測(cè)試遠(yuǎn)不如此,Go支持并行測(cè)試、基準(zhǔn)測(cè)試等。對(duì)于服務(wù)器-客戶端程序的測(cè)試也有支持。

關(guān)于Go的測(cè)試,這里有一個(gè)很好的教程:Go testing教程。

Go缺點(diǎn)

吹完了Go,作為一個(gè)新生語(yǔ)言,Go還是有很多缺點(diǎn)的。許多地方甚至為人詬病。作為一個(gè)轉(zhuǎn)型做Go的,我當(dāng)然希望Go越來(lái)越好,所以我們需要直面這些問(wèn)題。

包管理(go?module大法好)

在Go 1.11以前,Go使用GOPATH對(duì)Go項(xiàng)目進(jìn)行管理。這需要把當(dāng)前項(xiàng)目的所有依賴放到一個(gè)vendor目錄下。這對(duì)開(kāi)發(fā)人員來(lái)說(shuō)是一個(gè)噩夢(mèng):

  • 項(xiàng)目之間不能復(fù)用依賴
  • 依賴沒(méi)有版本控制,涉及到依賴的版本更新、回退、多版本共存等問(wèn)題時(shí),你會(huì)感到絕望
  • 如果一個(gè)依賴引用了其它依賴,你也會(huì)絕望的

還好社區(qū)有很多工具例如vgo可以在一定程度上解決這些問(wèn)題,但是還是很蛋疼。

終于,在Go 1.11以后,引入了go module,這個(gè)工具類似于Python的包管理,可以通過(guò)簡(jiǎn)單的命令來(lái)下載全局的依賴包。在項(xiàng)目中通過(guò)定義go.mod來(lái)引入依賴包,并且這個(gè)文件可以自動(dòng)生成。

有了go module,之前Go的包管理噩夢(mèng)就能在一定程度上得到解決,希望未來(lái)能得到大力推廣。

錯(cuò)誤處理

Go的錯(cuò)誤全部是通過(guò)返回來(lái)進(jìn)行的。Go并沒(méi)有傳統(tǒng)的try...catch。這意味著你在進(jìn)行錯(cuò)誤處理的時(shí)候需要寫(xiě)大量的這種代碼:

result, err := fun()
if err != nil {
    // handle error
}

當(dāng)然,Go支持函數(shù)式編程,你可以通過(guò)函數(shù)式編程的方式,通過(guò)wrapper模式來(lái)省略很多這樣的代碼。但是在很多業(yè)務(wù)場(chǎng)景,還是會(huì)很蛋疼的。

所以Go并不非常適合寫(xiě)業(yè)務(wù)代碼,目前行業(yè)內(nèi)寫(xiě)業(yè)務(wù)還是以Java這種語(yǔ)言為主。

當(dāng)然,Go實(shí)際上也有實(shí)現(xiàn)try...catch的方式,但是比較蛋疼。我們可以對(duì)比一下:

java:

public class TestTryCatch {

    public static void main(String[] args) {
        try {
            throw new Exception();
        } catch (Exception e) {
            System.out.Println("發(fā)生了錯(cuò)誤!");
        }
    }
}

go:

package main

import "fmt"

func main() {

    defer func() {
        if err := recover(); err != nil {
            fmt.Println("產(chǎn)生了錯(cuò)誤!")
        }
    }()

    panicFun()

}

func panicFun() {
    panic("我是錯(cuò)誤")
}

通過(guò)defer+recover可以修復(fù)Go的panic,但是我還是覺(jué)得沒(méi)有try...catch理解和用起來(lái)簡(jiǎn)單。

缺乏泛型

這是我個(gè)人非常討厭的一點(diǎn)。作為一個(gè)靜態(tài)語(yǔ)言,Go居然沒(méi)有泛型。這就像回到了幾十年前的Java一樣。

如果我想做一個(gè)稍微“通用”一點(diǎn)的功能,就會(huì)涉及到大量的類型向下轉(zhuǎn)型。我們知道,從JDK1.5開(kāi)始,Java就強(qiáng)烈不建議使用向下轉(zhuǎn)型而使用泛型,因?yàn)樗_實(shí)很不安全,即使在你事先知道對(duì)象類型的時(shí)候。

然而在2020年,Go仍然在使用大量的類型向下轉(zhuǎn)型。

雖然Go的設(shè)計(jì)理念是“少即是多”,Go的設(shè)計(jì)者將泛型和繼承作為一個(gè)整體從Go中刪去了。但是作為類型安全的一個(gè)保證,我認(rèn)為只要有interface{}(類似Java中的Object)的存在,泛型就必不可少。

希望在Go未來(lái)的版本看到泛型。

缺少框架(優(yōu)點(diǎn)?)

不像Java Spring,Go沒(méi)有一個(gè)大一統(tǒng)的框架。光是在web框架領(lǐng)域,就有很多選擇:

  • gin
  • beego
  • iris
  • echo

當(dāng)然,這也和Go自帶的庫(kù)很強(qiáng)大有關(guān)系,很多人不喜歡框架,覺(jué)得框架限制了他們發(fā)揮的空間;也有人覺(jué)得框架能夠快速開(kāi)發(fā),符合現(xiàn)代開(kāi)發(fā)的要求。

這點(diǎn)就因人而異了。

缺少更多的數(shù)據(jù)結(jié)構(gòu)

Go語(yǔ)言只提供了數(shù)組、Slice和map。對(duì)于棧、堆、隊(duì)列等其它數(shù)據(jù)結(jié)構(gòu),以及并發(fā)安全的數(shù)據(jù)結(jié)構(gòu),并沒(méi)有直接的支持。

我覺(jué)得這很大是因?yàn)镚o沒(méi)有泛型,如果要做一個(gè)通用的其它數(shù)據(jù)結(jié)構(gòu),就不得不處理interface{},這對(duì)類型安全來(lái)說(shuō)是一個(gè)災(zāi)難。

所以如果我們要實(shí)現(xiàn)某個(gè)數(shù)據(jù)結(jié)構(gòu),只能針對(duì)自己的struct手?jǐn)]了。

GC

在Go GC經(jīng)過(guò)了以下的發(fā)展階段:

  • Go 1.3之前:STW(Stop The World) 非常簡(jiǎn)陋的GC算法,在內(nèi)存超過(guò)閾值或定時(shí)的條件下,暫停所有任務(wù),執(zhí)行mark+sweep(標(biāo)記清除)。在高內(nèi)存場(chǎng)景下,這意味著任務(wù)的長(zhǎng)時(shí)間停頓,是一種災(zāi)難。
  • Go 1.3:Mark STW + Sweep。將mark和sweep分開(kāi)。但是也需要暫停所有任務(wù),但是暫停過(guò)程只進(jìn)行mark,mark之后恢復(fù)其它任務(wù),sweep通過(guò)協(xié)程異步進(jìn)行。這在一定程度上減少了GC的開(kāi)銷,減少STW的時(shí)間。
  • Go 1.5:三色標(biāo)記法。對(duì)mark進(jìn)行改進(jìn),使mark可以和用戶任務(wù)并發(fā)執(zhí)行。這種方法的mark操作是可以漸進(jìn)執(zhí)行的而不需每次都掃描整個(gè)內(nèi)存空間,可以進(jìn)一步減少STW的時(shí)間。
  • Go 1.8:混合寫(xiě)屏障(hybrid write barrier)允許堆棧掃描永久地使堆棧變黑(沒(méi)有STW并且沒(méi)有寫(xiě)入堆棧的障礙),這完全消除了堆棧重新掃描的需要,從而消除了對(duì)堆棧屏障的需求。使用這種方法可以將STW的時(shí)間降低到1毫秒以下

如果你的Go程序突然出現(xiàn)卡頓,就可能是GC的原因,就需要花時(shí)間去優(yōu)化,減少Go內(nèi)存的壓力。

在1.8版本以后,GC的延遲性下降了很多,但是因?yàn)樾枰⑿刑幚鞧C,線程間同步和多余的數(shù)據(jù)生成復(fù)制都會(huì)占用實(shí)際邏輯業(yè)務(wù)代碼運(yùn)行的時(shí)間。因此程序的吞吐量會(huì)下降,STW可以提高程序的吞吐量。

Go GC和Java JVM相比,差距還是很大的。但是Go畢竟是一個(gè)比較年輕的語(yǔ)言,給它時(shí)間進(jìn)行發(fā)展,未來(lái)GC一定會(huì)越來(lái)越好。

字段標(biāo)記災(zāi)難

看下面一段代碼,這是來(lái)自Go官方文檔的一個(gè)例子:

type Test struct {
    Label         *string             `protobuf:"bytes,1,req,name=label" json:"label,omitempty"`
    Type          *int32              `protobuf:"varint,2,opt,name=type,def=77" json:"type,omitempty"`
    Reps          []int64             `protobuf:"varint,3,rep,name=reps" json:"reps,omitempty"`
    Optionalgroup *Test_OptionalGroup `protobuf:"group,4,opt,name=OptionalGroup" json:"optionalgroup,omitempty"`
}

字段標(biāo)簽是Go提供的一個(gè)功能,用于給Go字段進(jìn)行注釋。在使用json的過(guò)程中,字段標(biāo)簽非常重要。

因?yàn)镚o中公共屬性首字母是大寫(xiě)的,而在json標(biāo)準(zhǔn)中字段首字母一般是小寫(xiě)的。而Go的標(biāo)準(zhǔn)庫(kù)json沒(méi)有這方面的自動(dòng)轉(zhuǎn)換,因此需要我們手動(dòng)進(jìn)行標(biāo)注。

如果涉及到更多協(xié)議相關(guān)的內(nèi)容,則標(biāo)簽會(huì)更長(zhǎng)。冗長(zhǎng)的標(biāo)簽會(huì)讓結(jié)構(gòu)體代碼的可讀性下降。

不是總結(jié)的總結(jié)

總體來(lái)說(shuō),Go還是一門(mén)不錯(cuò)的語(yǔ)言的。它寫(xiě)起來(lái)很像Python,卻有著Python遠(yuǎn)不及的性能。Go也有著很低的入門(mén)門(mén)檻。

當(dāng)然,Go最吸引人的地方就是它的協(xié)程了。協(xié)程的概念讓我們?cè)诰幊讨锌梢詭缀蹼S心所欲地創(chuàng)建并發(fā)任務(wù)而不用太多考慮開(kāi)銷。也讓我們的并發(fā)編程變得更加簡(jiǎn)單。

在高并發(fā)領(lǐng)域,Go可以說(shuō)是越來(lái)越受歡迎,很多公司都使用Go來(lái)構(gòu)建他們的基礎(chǔ)服務(wù)器設(shè)施。Go讓C/C++程序員遠(yuǎn)離構(gòu)建的痛苦,專注于開(kāi)發(fā)。

Go當(dāng)然還有很多缺點(diǎn),在業(yè)務(wù)編寫(xiě)上面,為很多人詬病。和Java還有很大的差別,生態(tài)圈也不如Java豐富。Go還有很多不為人知的陷阱(可以詳見(jiàn)我的“Go基礎(chǔ)筆記”系列)。

希望Go在未來(lái)能夠越來(lái)越好。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容