C+程序員是如何評價GO的
作者丨Murray
翻譯丨王江平
此文主要對GO語言的簡單語法做了詳細描述,並與C、C++、Java作了比較,以下為譯文。
我正在讀由Brian Kernighan和Alan Donovan編寫的《The Go Programming Language》這本書。 這是一本在語義、編排和常式選取方面都勘稱完美的語言類書籍。 沒有華而不實,而是精簡設計,摒除長篇闊論。
作為一個C ++和Java的狂熱開發者,並不是衷情於所有語言。這似乎是對C的一個改進版本,所以我寧願使用GO而不是C,但我仍然嚮往C ++的強大表達力。 我甚至懷疑,由於安全功能,Go無法實現C或C ++的原始性能,儘管這可能取決於編譯器優化。 但是,明智地選擇性能安全是非常有效的,特別是如果想獲得比Java更多的安全性和更高的性能。
我將選擇使用GO而不是C ++來實現一個使用並發和網路的概念程序的簡單證明。我會在以後的帖子中提及 Goroutines和 channels,一種方便的抽象,Go有HTTP請求的標準API。 並發性很強,在編寫網路代碼時,很容易選擇安全性。
下面是一些本人關於簡單功能的膚淺見解,其中大部分看起來都是對C的簡單改進。在
第2部分中,我將提到更高級的功能,我希望可以做一個關於並發的第3部分。 我強烈建議您閱讀本書以便正確理解這些問題。
歡迎友善的糾正和澄清。 免不了有幾個錯誤,希望沒有重大失誤。
行尾沒有分號
我們從簡單的入手。 與C,C ++或Java不同,Go在代碼行的末尾不需要分號。 所以出現下面的情形:
a = b
c = d
這對於將GO作為第一門編程語言來學的人來說可能更好。 對於分號問題可能需要一段時間來適應。
if 和 for 語句沒有括弧
這是另一個區別。 與 C 或 Java 不同,Go不將其條件放在括弧內。 這是一個小小的變化,感覺很隨意,可能會使C程序員感覺不舒服。
例如,在Go中,我們可以這樣寫:
for i := 0; i < 100; i++ {
...
}
if a == 2 {
...
}
用C語言是這樣:
for (int i = 0; i < 100; i++) {
...
}
if (a == 2) {
...
}
類型推斷
Go有類型推斷,從文本值或函數返回值,所以你不需要聲明編譯器能識別的類型。 這有點像C++的auto關鍵字(從C ++ 11開始)。 例如:
var a = 1 // An int.
var b = 1.0 // A float64.
var c = getThing()
還有一個 := 語法, 避免了 var 的需要, 雖然我不認為在語言中都需要:
a := 1 // An int.
b := 1.0 // A float64
d := getThing()
我喜歡使用C ++中的auto關鍵字進行類型推理,感覺使用沒有這個語法的語言真的很痛苦。 相比之下, java顯得有點繁瑣, 但也許 java 會實現這種用法。 我不明白為什麼C不能這樣做。 畢竟,它們最終允許在函數開始時聲明變數,所以改變是可能的。
名稱後的類型
GO 有變數/參數/函數名稱後的類型, 感覺相當隨意,儘管我猜想是有原因的,我個人可以適應。所以,在 C中可以這樣:
Foo foo = 2;
但是GO語言卻這樣寫:
var foo Foo = 2
保持一個更類似於 C 的語法將會使 C 開發人員輕鬆地入門。這些人往往不會接受語言的細微變化。
沒有隱式轉換
Go不存在類型之間的隱式轉換,例如int和uint,或者float和int。 == 和 != 也是如此。
因此,這不會被編譯:
var a int = -2
var b uint = a
var c int = b
var d float64 = 1.345
var e int = c
C編譯器警告可以捕獲其中的一些,但是 a)人們通常不會打開所有這些警告,並且它們不會將警告作為錯誤,b)警告不是嚴格的。
請注意,Go的類型是在變數 (或參數或函數) 名稱之後, 而不是之前。
注意一點,與Java不同,Go仍然具有無符號整數。 與C ++的標準庫不同,Go使用帶符號整數的大小和長度。真心希望C ++也能做到這一點。
沒有type聲明的隱式轉換
Go甚至不允許類型之間進行隱式轉換,在C中,只能是typedef。 所以,這不會編譯:
type Meters int
type Feet int
var a Meters = 100
var b Feet = a
在使用 typedef 時, 我想在 c 和 c++ 編譯器中看到這是一個警告。
但是,允許隱式地將文本 (非類型化) 值賦給類型變數, 但不能從基礎類型的實際類型變數中分配:
type Meters int
var a Meters = 100 // No problem.
var i int = 100
var b Meters = i // Will not compile.
沒有枚舉(enum)
GO語言沒有枚舉,應該使用帶 iota關鍵字的常量代替,雖然C ++代碼可能有這樣的:
enum class Continent {
NORTH_AMERICA,
SOUTH_AMERICA,
EUROPE,
AFRICA,
...
};
Continent c = Continent::EUROPE;
Continent d = 2; // Will not compile
在GO語言中,應該這樣:
type continent int
const (
CONTINENT_NORTH_AMERICA continent = iota
CONTINENT_SOUTH_AMERICA // Also a continent, with the next value via iota.
CONTINENT_EUROPE // Also a continent, with the next value via iota.
CONTINENT_AFRICA // Also a continent, with the next value via iota.
)
var c continent = CONTINENT_EUROPE
var d continent = 2 // But this works too.
請注意, 與 c++ 枚舉 (尤其是 C++11) 相比, 每個值的名稱必須有一個顯式前綴,並且編譯器不會阻止您將枚舉值分配給枚舉類型的變數。如果在switch/case模塊中漏寫,編譯器不會警告你,因為GO編譯器不會將這些值視為一組關聯的數值。
Switch/Case: 默認沒有fallthrough
譯者註:go裡面switch默認相當於每個case最後帶有break,匹配成功後不會自動向下執行其他case,而是跳出整個switch,但是可以使用fallthrough強制執行後面的case代碼,fallthrough不會判斷下一條case的expr結果是否為true。
在C和C ++中,您幾乎需要每個case之後有break語句。 否則,下面case塊中的代碼也將運行。 這可能是有用的,特別是當您希望相同的代碼響應多個值運行時,但這不是常見的情況。 在Go中,您必須添加一個明確的fallthrough關鍵字來獲取此行為,因此代碼在一般情況下更為簡潔。
Switch/Case: 不僅僅是基本類型
與C和C ++不同,在Go中,您可以切換任何可比較的值,而不僅僅是編譯時已知的值,如int,enums或其他constexpr值。 所以你可以打開字元串,例如:
switch str {
case "foo":
doFoo()
case "bar":
doBar()
}
這很方便,我想它仍然被編譯為高效的機器代碼,當它使用編譯時值。 C ++似乎已經抵制了這種方便,因為它不能總是像標準的 switch/case一樣有效,但是我認為當人們希望更加意識到映射,不必要地將switch/case語法與C的原始含義聯繫起來。
指針,但沒有間接引用運算符(->),沒有指針運算
Go具有普通類型和指針類型,並使用C和C ++中的*和&。 例如:
var a thing = getThing();
var p *thing = &a;
var b thing = *p; // Copy a by value, via the p pointer
與C ++一樣,new關鍵字返回一個指向新實例的指針:
var a *thing = new(thing)
var a thing = new(thing) // Compilation error
這類似於C ++,但不同於Java,其中任何非基本類型(例如,不是int或booleans)都可以通過引用(它只是看起來像一個值)被有效地使用,剛開始可能會使使用者混淆,通過允許這種疏忽的共享機制。
與C ++不同,您可以使用相同的點運算符調用值或指針上的方法:
var a *Thing = new(Thing) // You wouldn t normally specify the type.
var b Thing = *a
a.foo();
b.foo();
我喜歡這個。畢竟,編譯器知道這個類型是一個指針還是一個值,所以為什麼抱怨一下 a.。哪裡應該有 a-> 反之亦然?然而,隨著類型推斷,這可能會輕而易舉地掩蓋您的代碼是否處理指針(可能與其他代碼共享值)或值。我想在C ++中看到這一點,儘管智能指針會很尷尬。
Go中不能做指針運算。例如,如果您有一個數組,則不能通過自加到指針值並取消引用該數組。你必須通過索引來訪問數組元素,我認為這涉及邊界檢查。這避免了C和C ++代碼中可能發生的一些錯誤,當您的代碼訪問應用程序內存的意外部分時,會導致安全漏洞。
Go函數可以通過值或指針獲取參數。這就像C ++,但不同於Java,它總是使用非基本類型(非const)引用,儘管它可以看起來像初學者程序員一樣被值複製。我寧願使用代碼顯示通過函數簽名發生的事情的兩個選項,如C ++或Go。
像Java一樣,Go沒有const指針或const引用的概念。因此,如果您的函數將參數作為指針,為了提高效率,您的編譯器無法阻止您更改其指向的值。在Java中,這通常是通過創建不可變類型來完成的,並且許多Java類型(例如String)是不可變的,所以即使你願意也不能改變它們。但是我更喜歡語言支持,如C ++中的常量,指針/引用參數以及在運行時初始化的值。
References(引用), sometimes
Go似乎有引用(類似於值的指針),但僅適用於內置的slice,map和channel類型。 所以,例如這個函數可以改變其輸入的滑動參數,即使參數沒有被聲明為一個指針,調用者也可以看到這個改變:
func doThing(someSlice []int) {
someSlice[2] = 3;
}
在C ++中,這將是一個明顯的引用:
void doThing(Thing& someSlice) {
someSlice[2] = 3;
}
我不知道這是否是語言的基本特徵,或者只是關於這些類型的實現方式。 對於某些類型的行為來說, 這似乎有些混亂,我發現這個解釋有點凌亂。 方便固然很好,但一致性更加重要。
常量(const)
Go的const關鍵字不像C(很少有用)或C ++中的const,它表示在初始化後不應該更改變數的值。 它更像C ++的constexpr關鍵字(自C ++ 11),它在編譯時定義了值。 所以這有點像在C中通過#define定義的宏,而且是類型安全。 例如:
const pi = 3.14
請注意,我們不為const值指定類型,因此該值可以根據值的語法使用各種類型,有點像C宏#define。 但是我們可以通過指定一個類型來限制它:
const pi float64 = 3.14
與C ++中的constexpr不同,沒有可以在編譯時評估的constexpr函數或類型的概念,所以你不能這樣做:
const pi = calculate_pi()
你不能這樣做
type Point struct {
X int
Y int
}
const point = Point
雖然你可以使用一個簡單的類型,它的底層類型可以是const:
type Yards int
const length Yards = 100
只有for循環
Go語言中的只有for循環 - 沒有while或do-while循環。 與 C,C ++或Java語言相比,GO語言在這方面做了簡化,儘管現在有多種形式的for循環。
例如:
for i:= 0; i < 100; i ++ {
...
}
或者像C中的while循環一樣:
for keepGoing {
...
}
而for循環對於諸如字元串,切片或map之類的容器有一個基於範圍的語法,我稍後會提到:
for i, c := range things {
...
}
C ++有一個基於範圍的for循環,C ++ 11以後版本,但我喜歡Go可以(可選)給你索引和值。 (它為您提供索引,或索引和值,讓您忽略帶 _ variable 名稱的索引。)
本機(Unicode)字元串類型
Go具有內置的字元串類型,並且內置比較運算符,如==,!=和
str1:=「foo」
str2:= str1 +「bar」
GO語言源代碼總是UTF-8編碼,字元串文本可能包含非ASCII utf-8代碼點。 GO調用Unicode代碼點「runes(符文)」。
雖然內置的len()函數返回位元組數,而字元串的內置運算符[]運行在位元組上,但是有一個utf8包用於處理字元串作為符號(Unicode代碼點)。 例如:
str:=「foo」
l:= utf8.RuneCountInString(str)
而基於範圍的for循環在runes中處理,而不是位元組:
str:=「foo」
for _,r:= range str {
fmt.Println(「rune:%q」,r)
}
c++ 仍沒有標準等效項。
Slices(切片)
GO語言的Slices(切片)與 c 中動態分配的數組類似, 儘管它們實際上是底層數組的視圖, 而兩個切片可以是同一底層數組的不同部分的視圖。他他們感覺有點像C ++ 17或GSL :: span中的std :: string_view,但它們可以輕鬆調整大小,如C ++ 17中的std :: vector或Java中的ArrayList。
我們可以聲明一個像這樣的範圍, 並追加到它:
a := []int // A slice
a = append(a, 0)
數組(大小固定,不像切片)具有非常相似的語法:
a := [...]int // An array.
b := [5]int // Another array.
通過指針將數組傳遞給函數使用時必須注意,否則就會導致按值賦值。
與C ++中的std :: array或std :: vector不同,切片不是(深度)可比較或可複製的,這感覺相當不方便。
如果內置的append()函數需要比現有容量多(可能超過當前長度),則可以分配更大的底層數組。 所以你應該始終如此分配append()的結果:
a = append(a, 123)
我認為你不能將指針指向切片中的元素。 如果可以的話,垃圾收集系統需要保留以前的底層數組,直到你停止使用該指針。
與C或C ++數組不同,不同於使用std :: vector的operator [],嘗試訪問切片的無效索引將導致緊急(有效地崩潰),而不僅僅是未定義的行為。 我更喜歡這個,雖然我想像是邊界檢查有一些小的性能成本。
Maps(映射)
Go有一個內置的 map 類型。這大致相當於C ++的std :: map(平衡二叉樹)或std :: unordered_map(哈希表)。GO的maps顯然是哈希表,但是我不知道它們是單獨鏈接的哈希表(如std::unordered_map)還是開放定址哈希表(不幸的是,標準C ++中沒有什麼)。
顯然,哈希表中的 keys 必須是hashable 和 comparable。這本書提到了可比性,但是很少有事情是可比的,他們都很容易hashable。只有基本類型(int,float64,string等,但不是slice)或數據結構是可比較的,所以可以用它們作為一個關鍵。可以通過使用(或製作)您的值的哈希值的基本類型(如int或字元串)來解決此問題。我喜歡C ++需要一個std :: hash 專業化,儘管我希望寫一個更容易。
與C ++不同,您不能保留指向地圖中的元素的指針,因此更改值的一部分意味著將整個值複製回地圖,大概用另一個查找。顯然,當地圖必須增長時,完全避免無效指針的問題。 C ++可以讓您承擔風險,指定何時可能無效。
Go Maps顯然是一個比C更大的優勢,否則您必須使用一些第三方數據結構或編寫自己的數據,通常只有很少的類型安全。
看起來像下面這樣:
m := make(map[int]string)
m[3] = "three"
m[4] = "four"
Multiple return values(多個返回值)
Go中的函數可以有多個返回類型,更明顯的是輸出參數。 例如:
func getThings() (int, Foo) {
return 2, getFoo()
}
a, b := getThings()
這有點像在現代C ++中返回元組,特別是在C ++ 17中的結構化綁定:
std::tuple get_things() {
return make_tuple(2, get_foo());
}
auto [i, f] = get_things();
Garbage Collection(垃圾回收)
與 Java 一樣, GO 具有自動內存管理, 因此可以信任在使用完這些實例之前不會釋放它們, 也不需要顯式釋放它們。因此,可以放心地完成此操作,,而不必擔心以後釋放該實例:
func getThing() *Thing {
a := new(Thing)
...
return a
}
b := getThing()
b.foo()
你甚至可以這樣做,不用關心和了解實例是在堆棧還是堆上創建的:
func getThing() *Thing {
var a Thing
...
return &a
}
b := getThing()
b.foo()
我不知道Go如何避免循環引用或不需要的「泄漏」引用,因為Java或C ++將使用弱引用。
我不知道如何,或者如果,Go避免了Java由於垃圾收集而間歇性放緩的問題。 Go似乎是針對系統級代碼,所以我想它一定要做得更好。
然而,也像Java一樣,並且可能像所有垃圾收集一樣,這僅對於管理內存而非一般資源是有用的。 程序員通常很高興在代碼完成使用後一段時間內釋放內存,而不一定立即。 但其他資源,如文件描述符和資料庫連接,需要立即釋放。 一些事情,如互斥鎖,通常需要在明顯的範圍結束時釋放。 破壞者使之成為可能。 例如,在C ++中:
void Something::do_something() {
do_something_harmless();
{
std::lock_guard lock(our_mutex);
change_some_shared_state();
}
do_something_else_harmless();
}
Go不能這樣做,所以它有defer(),而是讓你指定一個事情發生在一個功能結束。 這是一個煩人的延遲與功能相關聯,而不是一般範圍。
func something() {
doSomethingHarmless()
ourMutex.Lock()
defer ourMutex.Unlock()
changeSomeSharedState()
// The mutex has not been released yet when this remaining code runs,
// so you d want to restrict the use of the resource (a mutex here) to
// another small function, and just call it in this function.
doSomethingElseHarmless()
}
這感覺像是一個尷尬的黑客,就像Java的試用資源一樣。
我更願意看到一種語言,以簡明的語法給我所有的範圍資源管理(包括析構函數),引用計數(如std :: shared_ptr )和垃圾回收,所以我可以有可預測的,明顯的, 但可靠,必要時釋放資源,垃圾收集時我不在乎。
當然,我不是假裝內存管理在C ++中很容易。 當它很困難時,這可能非常困難。 所以我明白垃圾收集的選擇。 我只是期望系統級語言提供更多。
不喜歡GO語言的地方
除了上面提到的較小的語法煩惱以及缺乏簡單的通用資源(而不僅僅是內存)管理之外,還有其它一些缺陷。
No generics(沒有泛型)
Go專註於類型安全,特別是對於數字類型,使得缺乏泛型令人驚訝。我可以記得在泛型之前使用Java的感覺是多麼令人沮喪,而這感覺差不多是尷尬的。沒有泛型,我很快發現自己不得不選擇缺乏類型安全或反覆重新實現每種類型的代碼,感覺就像這個語言作鬥爭。
我知道泛型是難以實現的,必須做出選擇,覺得GO語言能到達什麼程度(可能超過Java,但不如C ++),我知道Go將遠遠超過一個更好的C.但我認為泛型是不可避免的一次,像Go,你追求靜態型安全。
不知何故,切片和maps容器是通用的,可能是因為它們是內置類型。
Lack of standard containers(缺少標準容器)
Go在其標準庫中沒有隊列或堆棧。在C ++中,我定期使用std :: queue和std :: stack。我認為這些將需要仿製葯。人們可以使用go的切片(動態分配的數組)實現相同的功能,並且可以將其包裝在自己的類型中,但是類型只能包含特定類型,因此將為每種類型重新實現。或者您的容器可以容納{}類型的介面(顯然有點像Java對象或C ++ void *),放棄(靜態)類型安全。
點擊展開全文
※Kafka和消息隊列之間的超快速比較
※如何面試軟體工程師 看這篇就夠了
※運營總監講述Instapaper宕機原因及故障恢復過程詳解
※快哉!500 Startup創始人Dave McClure終下台,曾多次對女性創業者下骯髒之手
※深入分析一波,你們說的雲安全到底是什麼鬼?是傳統廠商的盒子的iso化?是雲廠商自身具備的安全能力?
TAG:CSDN |
※如何評價賓士GLA?
※如何評價NBA球員胡德?
※NBA球員是如何評價CBA?德克垃圾話讓CBA躺槍,老馬稱是我的生命
※如何評價《XXXHolic》這部作品?
※OPPO Find X消費者口碑如何?看看首批用戶是如何評價的
※佔領車聯網入口只是第一步,如何評價NXP與AliOS的合作?
※如何評價博瑞GE?
※EDG橫掃OMG後,觀眾用四個字評價表現!網友:基本操作
※如何評價SM的solo藝人
※如何評價iPhone X?
※如何評價NBA總決賽各個球員的表現?
※蘋果難得評價其他手機 首席設計師點贊OPPO Find X
※OPPO Find X好評如潮 網友評價稱全球最美的手機!
※經得起考驗!看專家如何評價KEMITEC
※史無前例,vivo LOGO PHONE上市一周競獲如此評價
※網友評價GAI夫妻像鳳凰傳奇,GAI卻用粗話回敬,想重蹈PGONE覆轍?
※神評論|如果評價韓國組合BIGBANG?網友:bigbang永遠的王者
※如何評價SUV型車?
※GAI首度評價吳亦凡DISS事件,我非常尊重吳亦凡老師
※TWICE私下和熒幕形象不同?合作過的工作人員公開對Twice的評價