Go 在 Google:在軟件工程服務中的設計

by Rob Pikegit

Abstract

這是Rob Pike在2012年10月25日在亞利桑那州圖森舉行的SPLASH 2012會議上發表的主題演講的修改版本。)程序員

Go編程語言是在2007年末構思出來的,它解決了咱們在Google開發軟件基礎架構時遇到的一些問題。今天的計算環境幾乎與建立所使用的語言(主要是C ++,Java和Python)的環境無關。多核處理器,網絡系統,大規模計算集羣和Web編程模型引入的問題正在解決,而不是正面解決。此外,規模發生了變化:今天的服務器程序包含數千萬行代碼,由數百甚至數千名程序員處理,而且天天都在進行更新。更糟糕的是,即便在大型編譯集羣上,構建時間也延長到幾分鐘甚至幾小時。github

Go的設計和開發旨在提升在此環境中的工做效率。除了內置併發和垃圾收集等衆所周知的方面外,Go的設計考慮因素包括嚴格的依賴關係管理,軟件架構隨系統增加的適應性以及跨組件邊界的穩健性。golang

本文解釋瞭如何在構建高效,編譯的編程語言時解決這些問題,這種語言感受輕巧愉快。將從Google面臨的現實問題中獲取示例和解釋。算法

Introduction

Go是Google開發的一種編譯的,併發的,垃圾收集的靜態類型語言。 這是一個開源項目:谷歌引入公共存儲庫repository而不是其它。編程

Go可擴展且高效。 有些程序員以爲工做頗有趣; 其餘人發現它缺少想象力,甚至無聊。在本文中,咱們將解釋爲何這些並不是相互矛盾的立場。 Go旨在解決Google軟件開發中遇到的問題,這種語言不是一種breakthrough research的語言,但倒是工程大型軟件項目的優秀工具。json

Go at Google

Go是一種由Google設計的編程語言,用於幫助解決Google的問題,Google存在很大問題。數組

硬件很大,軟件很大。 有數百萬行軟件,服務器主要使用C ++,其餘部分使用大量Java和Python。 成千上萬的工程師在代碼中工做,在包含全部軟件的單個樹的「頭部」,所以天天都會對樹的全部級別進行重大更改。 一個大型定製設計的分佈式構建系統使得這種規模的開發變得可行,但它仍然很大。緩存

固然,全部這些軟件都運行在數以萬計的機器上,這些機器被視爲適度數量的獨立網絡計算集羣。安全

簡而言之,谷歌的開發很大,可能很慢,並且每每很笨拙。 但它是有效的。

Go項目的目標是消除Google軟件開發的緩慢和笨拙,從而使流程更具生產力和可擴展性。 該語言是由讀寫,調試和維護大型軟件系統的人員設計的。 所以,Go的目的不是研究編程語言設計; 它是爲了改善設計師及其同事的工做環境。 Go更多地是關於軟件工程而不是編程語言研究。 或者重申一下,它是關於軟件工程服務中的語言設計。可是語言如何幫助軟件工程呢? 本文的其他部分是對該問題的回答。

Pain points

當Go發佈時,一些人聲稱它缺乏被認爲是現代語言的必要特徵或方法。若是沒有這些設施,Go怎麼可能有價值?咱們的答案是,Go確實解決了使大規模軟件開發變得困難的問題。這些問題包括:

  • 構建緩慢slow builds
  • 不受控制的依賴uncontrolled dependencies
  • 每一個程序員使用不一樣的語言子集
  • 程序理解友好性差(代碼難以閱讀,文檔記錄不當等)
  • 重複勞動duplication of effort
  • 更新代價cost of updates
  • 版本扭曲?version skew
  • 編寫自動工具的難度difficulty of writing automatic tools
  • 跨語言構建cross-language builds

語言的各個feature沒法解決這些問題。須要更大的軟件工程視角,在Go的設計中,咱們試圖關注這些問題的解決方案。

做爲一個簡單,自包含(self-contained)的例子,考慮程序結構化的表示。一些觀察者反對使用帶括號的Go的C-like塊結構,更喜歡使用Python或Haskell風格的縮進空格。可是,咱們在跟蹤由跨語言構建引發的構建和測試失敗方面擁有豐富的經驗,其中嵌入在另外一種語言中的Python片斷,例如經過SWIG調用,被周圍代碼的縮進的變化巧妙地和無形地破壞。所以,咱們的立場是,儘管縮縮進對於小的程序來講很好,可是它不能很好地擴展,代碼庫越大越異,它就越麻煩。最好放棄安全性和可靠性的得到,所以Go有塊結構brace-bounded blocks.。

Dependencies in C and C++

在處理包依賴性時出現了規模和其餘問題的更實質性的說明。咱們開始討論它們如何在C和C ++中工做。

ANSI C於1989年首次標準化,在標準頭文件中提高了#ifndef「guards」的概念。如今無處不在的想法是每一個頭文件都被條件編譯子句括起來,這樣文件能夠被屢次包含而沒有錯誤。例如,Unix頭文件<sys / stat.h>看起來像這樣:

/* Large copyright and licensing notice */
#ifndef _SYS_STAT_H_
#define _SYS_STAT_H_
/* Types and other definitions */
#endif
複製代碼

目的是C預處理器讀入文件但忽略文件的第二次和後續讀取的內容。第一次讀取文件時定義的符號_SYS_STAT_H_「守護」後面的調用。

這個設計有一些很好的屬性,最重要的是每一個頭文件均可以安全#include它的全部依賴項,即便其餘頭文件也包含它們。若是遵循該規則,則容許有序的代碼,例如,按字母順序對#include子句進行排序。

但它的體積很是糟糕。

1984年,在完成全部預處理時,觀察到ps.c的彙編(Unix ps命令的源代碼)#include <sys / stat.h> 37次。即便內容在執行此操做時被丟棄36次,大多數C實現也會打開文件,讀取並掃描37次。事實上,若是沒有很好的cleverness,C預處理器的潛在複雜宏語義就須要這種行爲。

對軟件的影響是C程序中#include子句的逐漸積累。它不會破壞添加它們的程序,而且很難知道什麼時候再也不須要它們。刪除#include並再次編譯程序甚至不足以測試它,由於另外一個#include自己可能包含一個#include來提取它。

從技術上講,它不必定是那樣的。經過使用#ifndef防禦來實現長期問題,Plan 9庫的設計者採用了不一樣的非ANSI標準方法。在Plan 9中,頭文件被禁止包含其餘#include子句;全部#includes都必須位於頂級C文件中。固然,這須要一些規則 - 程序員必須按正確的順序列出必要的依賴項一次 - 但文檔有所幫助,而且在實踐中它工做得很是好。結果是,不管C源文件有多少依賴關係,每一個#include文件在編譯該文件時都只讀取一次。固然,經過將其刪除也很容易看出#include是否必要:當且僅當依賴是沒必要要時,編輯的程序纔會編譯。

Plan 9方法最重要的結果是編譯速度更快:編譯所需的I / O數量遠遠少於使用#ifndef guards編譯程序的程序。

可是,在Plan 9以外,「守衛」方法是C和C ++的最佳實踐。實際上,C ++經過在更精細的粒度上使用相同的方法來加重問題。按照慣例,C ++程序一般由每一個類的一個頭文件構成,或者多是一小組相關類,一個比<stdio.h>小得多的分組。所以,依賴關係樹更復雜,反映了庫依賴關係,而不是完整的類型層次結構。此外,C ++頭文件一般包含真正的代碼類型,方法和模板聲明 - 而不只僅是C頭文件中典型的簡單常量和函數簽名。所以,不只C ++向編譯器推送更多內容,它推送的內容更難編譯,而且每次調用編譯器都必須從新處理此信息。在構建大型C ++二進制文件時,編譯器可能會被教授數千次如何經過處理頭文件<string>來表示字符串。 (據記載,1984年左右,Tom Cargill觀察到使用C預處理器進行依賴管理將是C ++的長期責任,應予以解決。)

在Google上構建單個C ++二進制文件能夠打開和讀取數百個單獨的頭文件數萬次。 2007年,Google的構建工程師負責編譯主要的Google二進制文件。該文件包含大約兩千個文件,若是簡單地鏈接在一塊兒,總計4.2兆字節。當擴展#includes時,超過8千兆字節被傳送到編譯器的輸入,每一個C ++源字節爆發2000字節。

做爲另外一個數據點,2003年Google的構建系統從單個Makefile轉移到具備更好管理,更明確的依賴關係的每一個目錄設計。典型的二進制文件縮小了大約40%的文件大小,只是記錄了更準確的依賴項。即使如此,C ++(或C語言)的屬性使得自動驗證這些依賴關係變得不切實際,而今天咱們仍然沒法準確理解大型Google C ++二進制文件的依賴性要求。

這些不受控制的依賴性和大規模的結果是在單個計算機上構建Google服務器二進制文件是不切實際的,所以建立了一個大型分佈式編譯系統。有了這個系統,涉及不少機器,不少緩存和不少複雜性(構建系統自己就是一個大型程序),谷歌的構建雖然仍然很麻煩,但卻很實用。

即便使用分佈式構建系統,大型Google構建仍然須要很長時間。使用前體分佈式構建系統,2007二進制文件須要45分鐘;今天版本的同一個程序須要27分鐘,但固然程序及其依賴性在過渡期間已經增加。擴展構建系統所需的工程工做幾乎沒法保持領先於正在構建的軟件的增加。

Enter Go

當構建緩慢時,有時間思考。 Go的起源神話代表,正是在這45分鐘構建之一中,構思了Go。人們認爲值得嘗試設計一種適合編寫大型Google程序(如Web服務器)的新語言,其軟件工程考慮因素能夠提升Google程序員的生活質量。

雖然到目前爲止的討論都集中在依賴性上,但還有許多其餘問題須要注意。在這種狀況下,任何語言成功的主要考慮因素是:

它必須大規模地work,對於具備大量依賴性的大型程序,以及大型程序員團隊。 它必須貌似,大體相似於C。在谷歌工做的程序員在職業生涯的早期階段,最熟悉程序語言,尤爲是來自C家族的程序語言。使程序員以新語言快速生產的須要意味着語言不能過於激進。 它必須是現代的。 C,C ++,在某種程度上,Java是至關陳舊的,是在多核機器,網絡和Web應用程序開發出現以前設計的。現代世界的某些功能能夠經過更新的方法更好地知足,例如內置併發。 有了這樣的背景,讓咱們從軟件工程的角度來看看Go的設計。

Dependencies in Go

既然咱們已經詳細瞭解了C和C ++中的依賴關係,那麼開始咱們之旅的一個好地方就是看看Go如何處理它們。依賴關係是由語言在語法和語義上定義的。它們是明確的,清晰的,「可計算的」,也就是說,易於編寫分析工具。

語法是,在package子句(下一節的主題)以後,每一個源文件可能有一個或多個import語句,包含import關鍵字和一個字符串常量,用於標識要導入到此源文件中的包(僅限) :

導入「encoding / json」 制定Go scale(依賴性)的第一步是語言定義未使用的依賴項是編譯時錯誤(不是警告,錯誤)。若是源文件導入它不使用的包,則程序將沒法編譯。這經過構造保證任何Go程序的依賴樹是精確的,它沒有無關的邊緣。反過來,這能夠保證在構建程序時不會編譯額外的代碼,從而最大限度地縮短編譯時間。

還有另外一個步驟,此次是編譯器的實現,這進一步保證了效率。考慮一個帶有三個包和這個依賴圖的Go程序:

package A imports package B; package B imports package C; package A does not import package C

這意味着包A僅經過使用B來傳遞使用C;也就是說,在A的源代碼中沒有提到來自C的標識符,即便A中使用的一些項目確實提到了C.例如,包A可能引用在B中定義的具備字段的結構類型。在C中定義的類型,但A不引用自身。做爲一個激勵性的例子,假設A導入一個格式化的I / O包B,它使用C提供的緩衝I / O實現,但A自己不調用緩衝的I / O.

要構建這個程序,首先要編譯C;依賴包必須在依賴它們的包以前構建。而後編譯B;最後A編譯,而後程序能夠連接。

編譯A時,編譯器會讀取B的目標文件,而不是源代碼。 B的該目標文件包含編譯器執行所需的全部類型信息

import "B" A的源代碼中的子句。該信息包括B的客戶端在編譯時須要的有關C的任何信息。換句話說,當編譯B時,生成的目標文件包括影響B的公共接口的B的全部依賴關係的類型信息。

這種設計具備以下重要影響:當編譯器執行import子句時,它只打開一個文件,該文件由import子句中的字符串標識。固然,這是讓人想起Plan 9 C(而不是ANSI C)依賴管理的方法,除了實際上編譯器在編譯Go源文件時編寫頭文件。儘管如此,該過程比Plan 9 C更自動,更高效:評估導入時讀取的數據只是「導出」數據,而不是通常程序源代碼。對總體編譯時間的影響可能很大,而且隨着代碼庫的增加而擴展。執行依賴圖並所以編譯的時間能夠比C和C ++的「包含文件」模型指數地少。

值得一提的是,這種依賴管理的通常方法並不是原創;這些想法能夠追溯到20世紀70年代,流經Modula-2和Ada等語言。在C系列中,Java具備這種方法的元素。

爲了使編譯更加高效,主體文件的排列使得導出數據是文件中的第一件事,所以編譯器能夠在到達該部分的末尾時當即中止讀取。

這種依賴管理方法是Go編譯比C或C ++編譯更快的最大緣由。另外一個因素是Go將導出數據放在目標文件中;某些語言須要做者編寫或編譯器生成包含該信息的第二個文件。這是打開文件的兩倍。在Go中,只有一個文件能夠打開以導入包。此外,單文件方法意味着導出數據(或C / C ++中的頭文件)永遠不會相對於目標文件過期。

爲了記錄,咱們測量了用Go編寫的大型Google程序的編譯,以瞭解源代碼扇出與以前完成的C ++分析相好比何。咱們發現它大約是40倍,比C ++好五十倍(而且更簡單,所以處理速度更快),但它仍然比咱們預期的要大。有兩個緣由。首先,咱們發現了一個錯誤:Go編譯器在導出部分生成了大量數據,而不須要在那裏。其次,導出數據使用能夠改進的詳細編碼。咱們計劃解決這些問題。

儘管如此,要作的事情要少五十分鐘纔會變成幾秒鐘,咖啡就會變成互動的構建。

Go依賴圖的另外一個特性是它沒有循環。該語言定義圖中不能有循環導入,編譯器和連接器都會檢查它們是否不存在。雖然它們偶爾會有用,但循環進口會引發大規模的重大問題。它們要求編譯器同時處理更大的源文件集,這會減慢增量構建。更重要的是,在咱們的經驗容許的狀況下,這些導入最終會將源代碼樹的大量內容糾纏成難以獨立管理的大型子元素,膨脹二進制文件以及複雜化初始化,測試,重構,發佈和其餘軟件開發任務。

缺少循環imports會致使偶爾的煩惱,但保持構建樹清晰,迫使包之間劃清界限。與Go中的許多設計策略同樣,它迫使程序員更早地思考一個更大規模的問題(在這種狀況下,包邊界),若是留到之後可能永遠不會使人滿意地解決。

經過標準庫的設計,花費了大量精力來控制依賴關係。複製一些代碼比爲一個函數拉入一個大型庫更好。 (若是出現新的核心依賴關係,系統構建中的測試會抱怨。)依賴性衛生賽過代碼重用。實踐中的一個例子是(低級)網絡包具備其本身的整數到十進制轉換例程,以免依賴於較大且依賴性較大的格式化I / O包。另外一個是字符串轉換包strconv具備'printable'字符定義的私有實現,而不是拉入大的Unicode字符類表; strconv尊重Unicode標準由包的測試驗證。

Packages

Go的包系統的設計將庫,命名空間和模塊的一些屬性組合到一個構造中。

每一個Go源文件,例如「encoding / json / json.go」,都以package子句開頭,以下所示:

package json 其中json是「包名」,一個簡單的標識符。包名稱一般簡潔。

要使用包,導入源文件經過import子句中的包路徑來標識它。 「path」的含義不是由語言指定的,但在實踐中,按照慣例,它是存儲庫中源包的斜槓分隔目錄路徑,此處:

import "encoding/json" 而後使用包名稱(與路徑不一樣)來限定導入源文件中包的項目:

var dec = json.NewDecoder(reader) 這種設計提供了清晰度人們可能老是從語法中判斷一個名稱是不是本地的:name vs pkg.Name。 (稍後會詳細介紹。)

對於咱們的示例,包路徑是「encoding / json」,而包名稱是json。在標準存儲庫以外,約定是將項目或公司名稱放在名稱空間的根目錄下:

import "google/base/go/log" 重要的是要認識到包路徑是惟一的,可是對包名沒有這樣的要求。路徑必須惟一標識要導入的包,而名稱只是包的客戶端如何引用其內容的約定。包名稱沒必要是惟一的,能夠經過在import子句中提供本地標識符來覆蓋每一個導入源文件。這兩個引用既調用本身的包日誌的引用包,但要將它們導入單個源文件,必須(本地)重命名:

import「log」//標準包 導入googlelog「google / base / go / log」// Google特定的包 每一個公司均可能有本身的日誌包,但不須要使包名稱惟一。偏偏相反:Go風格建議保持包裝名稱簡短,清晰明顯,而不是擔憂碰撞。

另外一個例子:Google的代碼庫中有許多服務器軟件包。

Remote packages

Go的包系統的一個重要特性是,包路徑一般是一個任意字符串,能夠經過識別爲存儲庫提供服務的站點的URL來選擇引用遠程存儲庫。

如下是如何使用github的doozer包。 go get命令使用go build工具從站點獲取存儲庫並進行安裝。 安裝後,能夠像任何常規包同樣導入和使用它。

$ go get github.com/4ad/doozer //用於獲取包的Shell命令

import「github.com/4ad/doozer」// Doozer客戶端的import語句

var client doozer.Conn //客戶端使用包 值得注意的是,go get命令以遞歸方式下載依賴項,只有由於依賴項是顯式的才能實現屬性。 此外,導入路徑空間的分配被委託給URL,這使得包的命名分散並所以可擴展,與其餘語言使用的集中式註冊表相反。 Go缺乏的一個功能是它不支持默認函數參數。 這是故意的簡化。 經驗告訴咱們,默認的參數使得經過添加更多參數來修補API設計缺陷變得太容易,致使過多的參數與難以解開甚至理解的交互。 缺乏默認參數須要定義更多的函數或方法,由於一個函數不能保存整個接口,但這會致使更容易理解的更清晰的API。 這些功能也須要單獨的名稱,這清楚地代表存在哪些組合,以及鼓勵更多地考慮命名,這是清晰度和可讀性的關鍵方面。

缺乏默認參數的一個緩解因素是Go對可變函數具備易於使用的類型安全支持。

Syntax

語法是編程語言的用戶界面。雖然它對語言的語義影響有限,這多是更重要的組成部分,但語法決定了語言的可讀性和清晰度。此外,語法對於工具相當重要:若是語言難以解析,則自動化工具很難編寫。

所以,Go的設計考慮了清晰度和工具,而且語法清晰。與C系列中的其餘語言相比,它的語法大小適中,只有25個關鍵字(C99有37個; C ++ 11有84個;數字繼續增加)。更重要的是,語法是規則的,所以易於解析(大多數狀況下;咱們可能已經修復了一些怪癖,但沒有及早發現)。與C和Java特別是C ++不一樣,Go能夠在沒有類型信息或符號表的狀況下進行解析;沒有特定類型的上下文。語法易於推理,所以工具易於編寫。

Go語法的一個細節令C程序員驚訝,聲明語法更接近Pascal而不是C語言。聲明的名稱出如今類型以前,而且有更多關鍵字: var fn func([] int)int type T struct {a,b int} 與C相比 int(* fn)(int []); struct T {int a,b; } 經過關鍵字引入的聲明更容易爲人和計算機解析,而且類型語法不是表達式語法,由於它在C中對解析有顯着影響:它增長了語法但消除了歧義。可是也有一個很好的反作用:對於初始化聲明,能夠刪除var關鍵字,只從表達式中獲取變量的類型。這兩個聲明是等價的;第二個是較短且慣用的: var buf * bytes.Buffer = bytes.NewBuffer(x)//顯式 buf:= bytes.NewBuffer(x)//派生 golang.org/s/decl-syntax上有一篇博客文章,其中詳細介紹了Go中聲明的語法以及爲何它與C有如此不一樣。

函數語法對於簡單函數來講很簡單。此示例聲明函數Abs,它接受類型爲T的單個變量x並返回單個float64值: func Abs(x T)float64 方法只是一個帶有特殊參數的函數,它的接收器可使用標準的「點」表示法傳遞給函數。方法聲明語法將接收器放在函數名稱前面的括號中。這是相同的函數,如今做爲類型T的方法:

func(x T)Abs()float64 這是一個帶有類型T參數的變量(閉包); Go擁有一流的功能和閉包:

negAbs:= func(x T)float64 {return -Abs(x)} 最後,在Go函數中能夠返回多個值。一種常見的狀況是將函數結果和錯誤值做爲一對返回,以下所示: ` func ReadByte() (c byte, err error)

c, err := ReadByte() if err != nil { ... } ` 咱們稍後會更多地討論錯誤。

Go缺乏的一個功能是它不支持默認函數參數。這是故意的簡化。經驗告訴咱們,默認的參數使得經過添加更多參數來修補API設計缺陷變得太容易,致使過多的參數與難以解開甚至理解的交互。缺乏默認參數須要定義更多的函數或方法,由於一個函數不能保存整個接口,但這會致使更容易理解的更清晰的API。這些功能也須要單獨的名稱,這清楚地代表存在哪些組合,以及鼓勵更多地考慮命名,這是清晰度和可讀性的關鍵方面。

缺乏默認參數的一個緩解因素是Go對可變函數具備易於使用的類型安全支持。

Naming

Go採用一種不尋常的方法來定義標識符的可見性,即包的客戶端使用標識符指定的項的能力。例如,與私有和公共關鍵字不一樣,在Go中,名稱自己包含信息:標識符的首字母大小寫的狀況決定了可見性。若是初始字符是大寫字母,則導出標識符(公共);不然不是:

大寫首字母:名稱對包的客戶可見 不然:包的客戶端看不到名稱(或_Name) 此規則適用於變量,類型,函數,方法,常量,字段......全部內容。這裏的全部都是它的。

這不是一個簡單的設計決定。咱們花了一年多的時間來努力定義符號來指定標識符的可見性。一旦咱們決定使用該名稱的狀況,咱們很快意識到它已成爲該語言最重要的屬性之一。畢竟,該名稱是該套餐的客戶使用的名稱;將可見性放​​在名稱而不是其類型中意味着在查看標識符時它是否老是清晰的,它是不是公共API的一部分。使用Go一段時間後,回到須要查找聲明以發現此信息的其餘語言時,會感到很麻煩。

結果再次清晰:程序源文本簡單地表達了程序員的意思。

另外一個簡化是Go具備很是緊湊的範圍層次結構:

universe(預先聲明的標識符,如int和string) package(包的全部源文件都在同一範圍內) 文件(僅用於包導入重命名;在實踐中不是很重要) 功能(一般) 塊(一般) 名稱空間或類或其餘包裝結構沒有空間。名稱來自Go中不多的地方,而且全部名稱都遵循相同的範圍層次結構:在源中的任何給定位置,標識符剛好表示一個語言對象,與其使用方式無關。 (惟一的例外是語句標籤,break語句的目標等;它們老是具備函數範圍。)

這具備清晰的後果。例如,請注意方法聲明瞭一個顯式接收器,而且必須使用它來訪問該類型的字段和方法。沒有暗示這一點。也就是說,老是寫道

rcvr.Field (其中rcvr是爲接收器變量選擇的任何名稱)所以該類型的全部元素老是在詞法上綁定到接收器類型的值。一樣,對於導入的名稱,始終存在包限定符;一我的寫io.Reader而不是Reader。這不只清楚,它還將標識符Reader釋放爲在任何包中使用的有用名稱。事實上,在標準庫中有多個導出的標識符,名稱爲Reader,或者就此而言是Printf,可是哪個被引用始終是明確的。

最後,這些規則結合起來保證除了頂級預約義名稱(例如int)(每一個名稱的第一個組件)以外,每一個名稱老是在當前包中聲明。

簡而言之,名字是本地的。在C,C ++或Java中,名稱y能夠引用任何內容。在Go中,y(或甚至Y)老是在包中定義,而x.Y的解釋是明確的:在本地查找x,Y屬於它。

這些規則提供了一個重要的擴展屬性,由於它們保證向包中添加導出的名稱永遠不會破壞該包的客戶端。命名規則將包解耦,提供縮放,清晰度和健壯性。

還有一個要提到的命名方面:方法查找始終只是名稱,而不是方法的簽名(類型)。換句話說,單個類型永遠不會有兩個具備相同名稱的方法。給定方法x.M,只有一個M與x相關聯。一樣,這樣能夠很容易地識別出僅使用名稱引用的方法。它還使方法調用的實現變得簡單。

Semantics

Go語句的語義一般是C語言。它是一個編譯的,靜態類型的過程語言,帶有指針等等。按照設計,對於習慣於C系列語言的程序員來講,它應該是熟悉的。在推出新語言時,目標受衆可以快速學習它是很重要的; root in Go in the C family有助於確保年輕的程序員(大多數人都懂Java,JavaScript和C語言)應該讓Go易於學習。

也就是說,Go對C語義進行了許多小改動,主要是爲了提供穩健性。這些包括:

  • 沒有指針運算

  • 沒有隱式數字轉換

  • 始終檢查數組邊界

  • 沒有類型別名(在類型X int以後,X和int是不一樣的類型而不是別名)

  • ++和 - 是語句而不是表達式

  • 賦值不是表達式

  • 獲取堆棧變量的地址是合法的(甚至鼓勵) 還有不少 還有一些更大的變化,遠離傳統的C,C ++甚至Java模型。這些包括語言支持:

  • 併發

  • 垃圾收集

  • 界面類型

  • 反射

  • 類型開關 如下部分簡要討論了Go,併發和垃圾收集中的兩個主題,主要是從軟件工程的角度。有關語言語義和用法的完整討論,請參閱golang.org網站上的許多資源。

Concurrency

併發性對於現代計算環境很是重要,其多核計算機運行具備多個客戶端的Web服務器,這可稱爲典型的Google程序。這種軟件並非特別適合C ++或Java,它在語言層面缺少足夠的併發支持。

Go體現了具備一流渠道的CSP變體。選擇CSP部分是因爲熟悉(咱們中的一我的已經研究了基於CSP思想的前任語言),但也由於CSP具備很容易添加到過程編程模型而不須要對該模型進行深入更改的特性。也就是說,給定類C語言,CSP能夠以大多數正交方式添加到語言中,提供額外的表達能力而不會限制語言的其餘用途。簡而言之,語言的其他部分能夠保持「普通」。

所以,該方法是獨立執行其餘常規程序代碼的功能的組合。

生成的語言容許咱們順利地將併發與計算結合起來。考慮一個必須驗證每一個傳入客戶端調用的安全證書的Web服務器;在Go中,很容易使用CSP構建軟件來管理客戶端做爲獨立執行的過程,可是具備高效編譯語言的所有功能可用於昂貴的加密計算。

總之,CSP適用於Go和Google。在編寫Web服務器,規範的Go程序時,該模型很是適合。

有一個重要的警告:在併發存在的狀況下,Go並非純粹的內存安全。共享是合法的,而且在通道上傳遞指針是慣用的(而且有效)。

一些併發和函數式編程專家對Go在併發計算的上下文中沒有采用一次寫入方法來估計語義而感到失望,例如Go並不像Erlang。一樣,緣由主要是關於問題領域的熟悉性和適用性。 Go的併發功能在大多數程序員熟悉的環境中運行良好。 Go支持簡單,安全的併發編程,但不由止編程錯誤。咱們根據慣例進行補償,培訓程序員將消息傳遞視爲全部權控制的一個版本。座右銘是「不要經過共享內存進行通訊,經過通訊共享內存」。

咱們對Go和併發編程都不熟悉的程序員的經驗有限,這代表這是一種實用的方法。程序員喜歡簡單性,支持併發性爲網絡軟件帶來了簡單性,而且簡化了強大的功能。

Garbage collection

對於系統語言,垃圾收集多是一個有爭議的feature,但咱們花了不多的時間就決定Go將是一個GC語言。 Go沒有明確的內存釋放操做:分配的內存返回池的惟一方法是經過垃圾收集器。

這是一個容易作出的決定,由於內存管理對語言在實踐中的運做方式產生了深遠的影響。在C和C ++中,過多的編程工做花費在內存分配和釋放上。由此產生的設計傾向於暴露可能隱藏的內存管理細節;相反,內存考慮限制了它們的使用方式。相比之下,垃圾收集使界面更容易指定。

此外,在一個併發的面嚮對象語言中,擁有自動內存管理幾乎是必不可少的,由於一塊內存的全部權在併發執行中傳遞時可能很難管理。將行爲與資源管理分開是很重要的。

因爲垃圾收集,語言更容易使用。

固然,垃圾收集帶來了巨大的成本:通常的開銷,延遲和實現的複雜性。儘管如此,咱們認爲程序員最常感覺到的好處超過了成本,這些成本主要由語言實現者承擔。

特別是Java做爲服務器語言的經驗使得一些人對面向用戶的系統中的垃圾收集感到緊張。開銷是沒法控制的,延遲可能很大,而且須要進行大量參數調整才能得到良好的性能。然而,去是不一樣的。該語言的屬性能夠緩解這些問題。固然不是所有,而是一些。

關鍵是Go爲程序員提供了經過控制數據結構佈局來限制分配的工具。考慮這個包含字節緩衝區(數組)的數據結構的簡單類型定義: type X struct {     a,b,c int     buf [256]byte } 在Java中,buf字段須要第二次分配並訪問第二級間接。可是,在Go中,緩衝區與包含結構一塊兒分配在單個內存塊中,而且不須要間接尋址。對於系統編程,此設計能夠具備更好的性能以及減小收集器已知的項目數量。在規模上它能夠產生顯着的差別。

做爲一個更直接的例子,在Go中提供二階分配器是簡單而有效的,例如競技場分配器,它分配大量結構並將它們與空閒列表連接在一塊兒。反覆使用這種小型結構的圖書館能夠經過適度的預先安排,不會產生垃圾,並且效率高,反應靈敏。

雖然Go是一種垃圾收集語言,可是,知識淵博的程序員能夠限制對收集器施加的壓力,從而提升性能。 (另外,Go安裝附帶了很好的工具來研究正在運行的程序的動態內存性能。)

爲了給程序員這種靈活性,Go必須支持咱們稱之爲堆中分配的對象的內部指針。上面示例中的X.buf字段位於結構體內,但捕獲此內部字段的地址是合法的,例如將其傳遞給I / O例程。在Java中,就像許多垃圾收集語言同樣,不可能像這樣構造一個內部指針,但在Go中它是慣用的。這個設計點會影響哪些採集算法可使用,而且可能會使它們變得更加困難,但仔細考慮後咱們認爲有必要容許內部指針,由於程序員的好處和減輕壓力的能力(也許更難)實施)收藏家。到目前爲止,咱們比較相似Go和Java程序的經驗代表,使用內部指針會對整體競技場大小,延遲和收集時間產生重大影響。

總之,Go是垃圾收集,但爲程序員提供了一些控制收集開銷的工具。

垃圾收集器仍然是一個活躍的發展領域。目前的設計是一個並行的標記和掃描收集器,仍有機會改善其性能甚至可能改進其設計。 (語言規範並無要求收集器的任何特定實現。)可是,若是程序員當心謹慎地使用內存,那麼當前的實現對於生產使用來講效果很好。

Composition not inheritance

Go採用了一種不尋常的方法來進行面向對象的編程,容許任何類型的方法,而不只僅是類,但沒有任何形式的基於類型的繼承,如子類化。這意味着沒有類型層次結構。這是一個有意的設計選擇。雖然已經使用類型層次結構來構建很是成功的軟件,但咱們認爲該模型已被過分使用而且值得退一步。

相反,Go有接口,這個想法已在其餘地方詳細討論過(例如參見research.swtch.com/interfaces),但這裏有一個簡短的總結。

在Go中,接口只是一組方法。例如,如下是標準庫中Hash接口的定義。

type Hash interface { Write(p []byte) (n int, err error) Sum(b []byte) []byte Reset() Size() int BlockSize() int } 現這些方法的全部數據類型都隱式地知足此接口;沒有工具聲明。也就是說,在編譯時靜態檢查接口滿意度,因此儘管這種解耦接口是類型安全的。

類型一般會知足許多接口,每一個接口對應於其方法的子集。例如,知足Hash接口的任何類型也知足Writer接口:

type Writer interface { Write(p []byte) (n int, err error) } 界面滿意度的這種流動性鼓勵了一種不一樣的軟件構建方法。但在解釋以前,咱們應該解釋爲何Go沒有子類化。

面向對象的編程提供了強大的洞察力:數據的行爲能夠獨立於該數據的表示而被推廣。當行爲(方法集)被修復時,該模型效果最好,可是一旦您對類型進行子類化並添加方法,行爲就再也不相同。若是相反,行爲集是固定的,例如在Go的靜態定義的接口中,行爲的一致性使數據和程序可以統一,正交和安全地組合。

一個極端的例子是Plan 9內核,其中全部系統數據項都實現了徹底相同的接口,一個由14種方法定義的文件系統API。即便在今天,這種均勻性也容許在其餘系統中不多達到必定程度的物體成分。例子比比皆是。這是一個:系統能夠將TCP堆棧(在Plan 9術語中)導入到沒有TCP甚至以太網的計算機上,並經過該網絡鏈接到具備不一樣CPU架構的計算機,導入其/ proc樹,以及運行本地調試器來對遠程進程進行斷點調試。這種行動在計劃9上是可行的,沒有什麼特別的。作這些事情的能力落在了設計以外;它不須要特殊的安排(而且都是在簡單的C中完成的)。

咱們認爲,這種構成風格的系統構造被類型層次推進設計的語言所忽略。類型層次結構致使脆弱的代碼。層次結構必須儘早設計,一般做爲設計程序的第一步,一旦編寫程序,早期決策可能難以改變。所以,該模型鼓勵早期過分設計,由於程序員試圖預測軟件可能須要的每種可能的用途,並在如下狀況下添加類型和抽象層。這是顛倒的。系統交互的方式應該隨着它的增加而適應,而不是在時間的早晨獲得修復。

所以,Go鼓勵組合而不是繼承,使用簡單的,一般是單方法的接口來定義瑣碎的行爲,這些行爲充當組件之間清晰,易於理解的界限。

考慮上面顯示的Writer接口,它在包io中定義:任何具備此簽名的Write方法的項都適用於補充的Reader接口:

type Reader interface { Read(p []byte) (n int, err error) } 這兩種互補方法容許類型安全連接具備豐富的行爲,如通用Unix管道。文件,緩衝區,網絡,加密器,壓縮器,圖像編碼器等均可以鏈接在一塊兒。 Fprintf格式化的I / O例程採用io.Writer而不是像C同樣使用FILE *。格式化的打印機不知道它寫的是什麼;它能夠是圖像編碼器,其又寫入壓縮器,該壓縮器又寫入加密器,該加密器又寫入網絡鏈接。

接口組合是一種不一樣的編程風格,習慣於類型層次結構的人須要調整他們的思路才能作好,但結果是設計的適應性很難經過類型層次結構來實現。

另請注意,消除類型層次結構也會消除一種依賴關係層次結構。界面滿意度容許程序在沒有預約合同的狀況下有機增加。它是一種線性增加形式;對接口的更改僅影響該接口的直接客戶端;沒有子樹能夠更新。缺少工具聲明會擾亂一些人,但它使程序可以天然,優雅和安全地發展。

Go的接口對程序設計有重大影響。咱們看到的一個地方是使用帶有接口參數的函數。這些不是方法,而是功能。一些例子應該說明它們的力量。 ReadAll返回一個字節切片(數組),其中包含可從io.Reader讀取的全部數據: func ReadAll(r io.Reader)([] byte,error) Wrappers-接口和返回接口的功能也很廣泛。這是一些原型。 LoggingReader記錄傳入Reader上的每一個Read調用。 LimitingReader在n個字節後中止讀取。 ErrorInjector經過模擬I / O錯誤來輔助測試。還有更多。 func LoggingReader(r io.Reader)io.Reader func LimitingReader(r io.Reader,n int64)io.Reader func ErrorInjector(r io.Reader)io.Reader 這些設計與分層的,子類型繼承的方法徹底不一樣。它們更寬鬆(甚至是臨時的),有機的,分離的,獨立的,所以可擴展。

Errors

Go沒有傳統意義上的異常工具,也就是說,沒有與錯誤處理相關的控制結構。 (Go確實提供了處理異常狀況的機制,例如除以零。一對稱爲恐慌和恢復的內置函數容許程序員防止這些事情。可是,這些函數故意笨拙,不多使用,而且沒有集成到例如,Java庫使用異常的方式。)

錯誤處理的關鍵語言功能是一個名爲error的預約義接口類型,它表示一個返回字符串的Error方法的值:

type error interface { Error() string } 使用錯誤類型返回錯誤的描述。結合函數返回多個值的能力,很容易返回計算結果和錯誤值(若是有的話)。例如,等效於C的getchar不會返回EOF的帶外值,也不會拋出異常;它只是在字符旁邊返回一個錯誤值,nil錯誤值表示成功。如下是緩衝I / O包的bufio.Reader類型的ReadByte方法的簽名: func (b *Reader) ReadByte() (c byte, err error) 這是一個簡單明瞭的設計,易於理解。錯誤只是值和程序使用它們計算,由於它們將使用任何其餘類型的值進行計算。

故意選擇不在Go中加入例外。雖然許多批評者不一樣意這一決定,但咱們認爲有幾個緣由能夠促成更好的軟件。

首先,計算機程序中的錯誤沒有什麼特別之處。例如,沒法打開文件是一個常見的問題,不值得特殊的語言結構;若是和返回都沒問題。

f, err := os.Open(fileName) if err != nil { return err } 此外,若是錯誤使用特殊控制結構,則錯誤處理會扭曲處理錯誤的程序的控制流。 try-catch-finally塊的相似Java的樣式交織了多個重疊的控制流,這些控制流以複雜的方式進行交互。雖然相比之下,Go使檢查錯誤更加冗長,但顯式設計使控制流程直截了當 - 字面意義。

毫無疑問,生成的代碼能夠更長,但這些代碼的清晰度和簡單性抵消了它的冗長。顯式錯誤檢查會強制程序員在出現錯誤時考慮錯誤並處理錯誤。異常使得忽略它們而不是處理它們太容易了,將調試堆棧向上傳遞,直到解決問題或診斷問題爲時已晚。

Tools

軟件工程須要工具。每種語言都在具備其餘語言和無數工具的環境中運行,以編譯,編輯,調試,配置,測試和運行程序。

Go的語法,包系統,命名約定和其餘功能旨在使工具易於編寫,而且庫包括詞法分析器,解析器和類型檢查器。

操做Go程序的工具很容易編寫,已經建立了許多這樣的工具,其中一些工具對軟件工程產生了有趣的影響。

其中最着名的是gofmt,Go源代碼格式化程序。從項目開始,咱們打算用機器格式化Go程序,消除程序員之間的整個論點:我如何佈置代碼? Gofmt在咱們編寫的全部Go程序上運行,大多數開源社區也使用它。它做爲代碼存儲庫的「預提交」檢查運行,以確保全部簽入的Go程序的格式相同。

Gofmt常常被用戶稱爲Go的最佳功能之一,即便它不是語言的一部分。 gofmt的存在和使用意味着社區從一開始就一直將Go代碼視爲gofmt格式化,所以Go程序只有一種風格,如今每一個人都很熟悉。統一的表示使代碼更易於閱讀,所以能夠更快地進行操做。不花費在格式上的時間是節省時間。 Gofmt還會影響可伸縮性:因爲全部代碼看起來都相同,所以團隊能夠更輕鬆地協同工做或與其餘代碼協同工做。

Gofmt啓用了另外一類咱們沒有預見到的工具。該程序經過解析源代碼並從解析樹自己從新格式化它來工做。這使得在格式化以前編輯解析樹成爲可能,所以出現了一套自動重構工具。它們易於編寫,能夠在語義上豐富,由於它們直接在解析樹上工做,並自動生成規範格式的代碼。

第一個例子是gofmt自己的-r(重寫)標誌,它使用簡單的模式匹配語言來啓用表達式級別的重寫。例如,有一天咱們爲切片表達式的右側引入了一個默認值:長度自己。整個Go源代碼樹已更新爲使用此命令的默認值:

gofmt -r'a [b:len(a)] - > a [b:]' 關於這種轉換的一個關鍵點是,由於輸入和輸出都是規範格式,因此對源代碼所作的惟一更改是語義的。

若是語句在換行符處結束,當語言再也不須要分號做爲語句終止符時,相似但更復雜的過程容許使用gofmt來更新樹。

另外一個重要的工具是gofix,它運行用Go編寫的樹重寫模塊,所以可以進行更高級的重構。 gofix工具容許咱們對API和語言功能進行全面更改,直到Go 1的發佈,包括更改從地圖中刪除條目的語法,用於操做時間值的徹底不一樣的API等等。隨着這些更改的推出,用戶能夠經過運行simple命令更新全部代碼

gofix 請注意,即便舊代碼仍然有效,這些工具也容許咱們更新代碼。所以,隨着庫的發展,Go存儲庫很容易保持最新。能夠快速自動地棄用舊API,所以只須要維護一個版本的API。例如,咱們最近改變了Go的協議緩衝區實現以使用「getter」函數,這些函數以前不在接口中。咱們在全部Google的Go代碼上運行gofix來更新全部使用協議緩衝區的程序,如今只有一個版本的API在使用中。在Google的代碼庫中,對C ++或Java庫進行相似的完全更改幾乎是不可行的。

標準Go庫中存在解析包也啓用了許多其餘工具。例子包括go工具,它管理程序構建,包括從遠程存儲庫獲取包; godoc文檔提取程序,用於驗證在庫更新時是否維護API兼容性合同的程序等等。

儘管在語言設計的背景下不多說起這些工具,但它們是語言生態系統中不可或缺的一部分,而Go的設計考慮了工具,這對語言,庫和它的開發產生了巨大的影響。社區。

Conclusion

Go的用途正在Google內部增加。

幾個面向用戶的大型服務使用它,包括youtube.com和dl.google.com(提供Chrome,Android和其餘下載的下載服務器),以及咱們本身的golang.org。固然,不少小的都是使用Google App Engine對Go的原生支持構建的。

許多其餘公司也使用Go;列表很長,但一些更爲人所知的是:

BBC全球 典範 Heroku的 諾基亞 的SoundCloud 看起來Go正在實現其目標。儘管如此,宣佈它成功還爲時尚早。咱們尚未足夠的經驗,尤爲是大型程序(數百萬行代碼),以瞭解構建可擴展語言的嘗試是否獲得了回報。但全部指標都是正面的。

在較小的範圍內,一些小的東西不太正確,可能會在之後的(Go 2?)版本的語言中進行調整。例如,有太多形式的變量聲明語法,程序員很容易被非零接口內的nil值行爲所迷惑,而且有許多庫和接口細節可使用另外一輪設計。

值得注意的是,gofix和gofmt讓咱們有機會在Go版本1的引導期間解決許多其餘問題。由於它如今比設計師想要的更接近於沒有這些工具的狀況,這些都是由語言設計啓用的。

但並不是全部事情都獲得瞭解決。咱們還在學習(但如今語言已被凍結)。

語言的一個重要缺點是實現仍然須要工做。編譯器生成的代碼和運行時的性能應該更好,並繼續工做。已經取得了進展;實際上,與2012年初的Go版本1的第一個版本相比,今天的開發版本的一些基準測試顯示性能翻了一番。

Summary

軟件工程指導了Go的設計。除了大多數通用編程語言以外,Go還旨在解決咱們在構建大型服務器軟件時遇到的一系列軟件工程問題。另外一方面,這可能會讓Go聽起來至關沉悶和工業化,但事實上,整個設計中對清晰度,簡潔性和可組合性的關注反而產生了一種高效,有趣的語言,許多程序員都以爲這種語言具備表現力和強大功能。

致使這種狀況的屬性包括:

  • 清除依賴關係
  • 語法清晰
  • 清晰的語義
  • 繼承的構成
  • 編程模型提供的簡單性(垃圾收集,併發)
  • 簡單的工具(go工具,gofmt,godoc,gofix) 若是您尚未嘗試過Go,咱們建議您這樣作。 golang.org
相關文章
相關標籤/搜索