go語言是什麼時候出現的

2022-12-28 18:00:18

go語言是谷歌2009釋出的開源程式語言。Go語言最初由Google公司的Robert Griesemer、Ken Thompson和Rob Pike三個大牛於2007年開始設計發明,並於2009年正式對外發布;三名初始人最終的目標是設計一種適應網路和多核時代的C語言,所以Go語言很多時候被描述為「類C語言」,或者是「21世紀的C語言」,Go從C繼承了相似的語法、程式設計思想等。

本教學操作環境:windows7系統、GO 1.18版本、Dell G3電腦。

1. Go語言的出現

在具體學習go語言的基礎語法之前,我們來了解一下go語言出現的時機及其特點。

Go語言最初由Google公司的Robert Griesemer、Ken Thompson和Rob Pike三個大牛於2007年開始設計發明,於2009年11月開源,一開始在google內部作為一個20%的專案執行。

Go 語言起源 2007 年,並於 2009 年正式對外發布。它從 2009 年 9 月 21 日開始作為谷歌公司 20% 兼職專案,即相關員工利用 20% 的空餘時間來參與 Go 語言的研發工作。

三名初始人最終的目標是設計一種適應網路和多核時代的C語言,所以Go語言很多時候被描述為「類C語言」,或者是「21世紀的C語言」,當然從各種角度看,Go語言確實是從C語言繼承了相似的表示式語法、控制流結構、基礎資料型別、呼叫引數傳值、指標等諸多程式設計思想。但是Go語言更是對C語言最徹底的一次揚棄,它捨棄了C語言中靈活但是危險的指標運算,還重新設計了C語言中部分不太合理運運算元的優先順序,並在很多細微的地方都做了必要的打磨和改變。

2. go版本的hello world

在這一部分我們只是使用「hello world」的程式來向大家介紹一下go語言的所編寫的程式的基本組成。

package main
import "fmt"
func main() {
	// 終端輸出hello world
	fmt.Println("Hello world!")
}
登入後複製

和C語言相似,go語言的基本組成有:

  • 包宣告,編寫原始檔時,必須在非註釋的第一行指明這個檔案屬於哪個包,如package main
  • 引入包,其實就是告訴Go 編譯器這個程式需要使用的包,如import "fmt"其實就是引入了fmt包。
  • 函數,和c語言相同,即是一個可以實現某一個功能的函數體,每一個可執行程式中必須擁有一個main函數。
  • 變數,Go 語言變數名由字母、數位、下劃線組成,其中首個字元不能為數位。
  • 語句/表示式,在 Go 程式中,一行代表一個語句結束。每個語句不需要像 C 家族中的其它語言一樣以分號 ; 結尾,因為這些工作都將由 Go 編譯器自動完成。
  • 註釋,和c語言中的註釋方式相同,可以在任何地方使用以 // 開頭的單行註釋。以 /* 開頭,並以 */ 結尾來進行多行註釋,且不可以巢狀使用,多行註釋一般用於包的檔案描述或註釋成塊的程式碼片段。

需要注意的是:識別符號是用來命名變數、型別等程式實體。一個識別符號實際上就是一個或是多個字母和數位、下劃線_組成的序列,但是第一個字元必須是字母或下劃線而不能是數位。

  • 當識別符號(包括常數、變數、型別、函數名、結構欄位等等)以一個大寫字母開頭,如:Group1,那麼使用這種形式的識別符號的物件就可以被外部包的程式碼所使用(使用者端程式需要先匯入這個包),這被稱為匯出(像物件導向語言中的 public);

  • 識別符號如果以小寫字母開頭,則對包外是不可見的,但是他們在整個包的內部是可見並且可用的(像物件導向語言中的 protected)。

3. 資料型別

在 Go 程式語言中,資料型別用於宣告函數和變數。

資料型別的出現是為了把資料分成所需記憶體大小不同的資料,程式設計的時候需要用巨量資料的時候才需要申請大記憶體,就可以充分利用記憶體。具體分類如下:

型別詳解
布林型布林型的值只可以是常數 true 或者 false。
數位型別整型 int 和浮點型 float。Go 語言支援整型和浮點型數位,並且支援複數,其中位的運算採用二補數。
字串型別字串就是一串固定長度的字元連線起來的字元序列。Go 的字串是由單個位元組連線起來的。Go 語言的字串的位元組使用 UTF-8 編碼標識 Unicode 文字。
派生型別(a) 指標型別(Pointer)(b) 陣列型別© 結構化型別(struct)(d) Channel 型別(e) 函數型別(f) 切片型別(g) 介面型別(interface)(h) Map 型別

3.0 定義變數

宣告變數的一般形式是使用 var 關鍵字,具體格式為:var identifier typename。如下的程式碼中我們定義了一個型別為int的變數。

package main
import "fmt"
func main() {
	var a int = 27
	fmt.Println(a);
}
登入後複製

3.0.1 如果變數沒有初始化

在go語言中定義了一個變數,指定變數型別,如果沒有初始化,則變數預設為零值。零值就是變數沒有做初始化時系統預設設定的值

型別零值
數值型別0
布林型別false
字串「」(空字串)

3.0.2 如果變數沒有指定型別

在go語言中如果沒有指定變數型別,可以通過變數的初始值來判斷變數型別。如下程式碼

package main
import "fmt"
func main() {
    var d = true
    fmt.Println(d)
}
登入後複製

3.0.3 :=符號

當我們定義一個變數後又使用該符號初始化變數,就會產生編譯錯誤,因為該符號其實是一個宣告語句。

使用格式:typename := value

也就是說intVal := 1相等於:

var intVal int 
intVal =1
登入後複製

3.0.4 多變數宣告

可以同時宣告多個型別相同的變數(非全域性變數),如下圖所示:

var x, y int
var c, d int = 1, 2
g, h := 123, "hello"
登入後複製

關於全域性變數的宣告如下:
var ( vname1 v_type1 vname2 v_type2 )
具體舉例如下:

var ( 
    a int
    b bool
)
登入後複製

3.0.5 匿名變數

匿名變數的特點是一個下畫線_,這本身就是一個特殊的識別符號,被稱為空白識別符號。它可以像其他識別符號那樣用於變數的宣告或賦值(任何型別都可以賦值給它),但任何賦給這個識別符號的值都將被拋棄,因此這些值不能在後續的程式碼中使用,也不可以使用這個識別符號作為變數對其它變數進行賦值或運算。

使用匿名變數時,只需要在變數宣告的地方使用下畫線替換即可。

範例程式碼如下:

    func GetData() (int, int) {
        return 10, 20
    }
    func main(){
        a, _ := GetData()
        _, b := GetData()
        fmt.Println(a, b)
    }
登入後複製

需要注意的是匿名變數不佔用記憶體空間,不會分配記憶體。匿名變數與匿名變數之間也不會因為多次宣告而無法使用。

3.0.6 變數作用域

作用域指的是已宣告的識別符號所表示的常數、型別、函數或者包在原始碼中的作用範圍,在此我們主要看一下go中變數的作用域,根據變數定義位置的不同,可以分為一下三個型別:

  • 函數內定義的變數為區域性變數,這種區域性變數的作用域只在函數體內,函數的引數和返回值變數都屬於區域性變數。這種變數在存在於函數被呼叫時,銷燬於函數呼叫結束後。

  • 函數外定義的變數為全域性變數,全域性變數只需要在一個原始檔中定義,就可以在所有原始檔中使用,甚至可以使用import引入外部包來使用。全域性變數宣告必須以 var 關鍵字開頭,如果想要在外部包中使用全域性變數的首字母必須大寫

  • 函數定義中的變數成為形式引數,定義函數時函數名後面括號中的變數叫做形式引數(簡稱形參)。形式引數只在函數呼叫時才會生效,函數呼叫結束後就會被銷燬,在函數未被呼叫時,函數的形參並不佔用實際的儲存單元,也沒有實際值。形式引數會作為函數的區域性變數來使用

3.1 基本型別

型別描述
uint8 / uint16 / uint32 / uint64無符號 8 / 16 / 32 / 64位元整型
int8 / int16 / int32 / int64有符號8 / 16 / 32 / 64位元整型
float32 / float64IEEE-754 32 / 64 位浮點型數
complex64 / complex12832 / 64 位實數和虛數
byte類似 uint8
rune類似 int32
uintptr無符號整型,用於存放一個指標

以上就是go語言基本的資料型別,有了資料型別,我們就可以使用這些型別來定義變數,Go 語言變數名由字母、數位、下劃線組成,其中首個字元不能為數位。

3.2 指標

與C相同,Go語言讓程式設計師決定何時使用指標。變數其實是一種使用方便的預留位置,用於參照計算機記憶體地址。Go 語言中的的取地址符是&,放到一個變數前使用就會返回相應變數的記憶體地址。

指標變數其實就是用於存放某一個物件的記憶體地址。

3.2.1 指標宣告和初始化

和基礎型別資料相同,在使用指標變數之前我們首先需要申明指標,宣告格式如下:var var_name *var-type,其中的var-type 為指標型別,var_name 為指標變數名,* 號用於指定變數是作為一個指標。

程式碼舉例如下:

var ip *int        /* 指向整型*/
var fp *float32    /* 指向浮點型 */
登入後複製

指標的初始化就是取出相對應的變數地址對指標進行賦值,具體如下:

   var a int= 20   /* 宣告實際變數 */
   var ip *int        /* 宣告指標變數 */

   ip = &a  /* 指標變數的儲存地址 */
登入後複製

3.2.2 空指標

當一個指標被定義後沒有分配到任何變數時,它的值為 nil,也稱為空指標。它概念上和其它語言的null、NULL一樣,都指代零值或空值。

3.3 陣列

和c語言相同,Go語言也提供了陣列型別的資料結構,陣列是具有相同唯一型別的一組已編號且長度固定的資料項序列,這種型別可以是任意的原始型別例如整型、字串或者自定義型別。

3.3.1 宣告陣列

Go 語言陣列宣告需要指定元素型別及元素個數,語法格式如下:

var variable_name [SIZE] variable_type

以上就可以定一個一維陣列,我們舉例程式碼如下:

var balance [10] float32
登入後複製

3.3.2 初始化陣列

陣列的初始化方式有不止一種方式,我們列舉如下:

  • 直接進行初始化:var balance = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}

  • 通過字面量在宣告陣列的同時快速初始化陣列:balance := [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}

  • 陣列長度不確定,編譯器通過元素個數自行推斷陣列長度,在[ ]中填入...,舉例如下:var balance = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}balance := [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}

  • 陣列長度確定,指定下標進行部分初始化:balanced := [5]float32(1:2.0, 3:7.0)

注意:

  • 初始化陣列中 {} 中的元素個數不能大於 [] 中的數位。
    如果忽略 [] 中的數位不設定陣列大小,Go 語言會根據元素的個數來設定陣列的大小。

3.3.3 go中的陣列名意義

在c語言中我們知道陣列名在本質上是陣列中第一個元素的地址,而在go語言中,陣列名僅僅表示整個陣列,是一個完整的值,一個陣列變數即是表示整個陣列。

所以在go中一個陣列變數被賦值或者被傳遞的時候實際上就會複製整個陣列。如果陣列比較大的話,這種複製往往會佔有很大的開銷。所以為了避免這種開銷,往往需要傳遞一個指向陣列的指標,這個陣列指標並不是陣列。關於陣列指標具體在指標的部分深入的瞭解。

3.3.4 陣列指標

通過陣列和指標的知識我們就可以定義一個陣列指標,程式碼如下:

var a = [...]int{1, 2, 3} // a 是一個陣列
var b = &a                // b 是指向陣列的指標
登入後複製

陣列指標除了可以防止陣列作為引數傳遞的時候浪費空間,還可以利用其和for range來遍歷陣列,具體程式碼如下:

for i, v := range b {     // 通過陣列指標迭代陣列的元素
    fmt.Println(i, v)
}
登入後複製

具體關於go語言的迴圈語句我們在後文中再進行詳細介紹。

3.4 結構體

通過上述陣列的學習,我們就可以直接定義多個同型別的變數,但這往往也是一種限制,只能儲存同一種型別的資料,而我們在結構體中就可以定義多個不同的資料型別。

3.4.1 宣告結構體

在宣告結構體之前我們首先需要定義一個結構體型別,這需要使用type和struct,type用於設定結構體的名稱,struct用於定義一個新的資料型別。具體結構如下:

type struct_variable_type struct {
   member definition
   member definition
   ...
   member definition
}
登入後複製

定義好了結構體型別,我們就可以使用該結構體宣告這樣一個結構體變數,語法如下:

variable_name := structure_variable_type {value1, value2...valuen}

variable_name := structure_variable_type { key1: value1, key2: value2..., keyn: valuen}
登入後複製

3.4.2 存取結構體成員

如果要存取結構體成員,需要使用點號 . 操作符,格式為:結構體變數名.成員名。舉例程式碼如下:

package main

import "fmt"

type Books struct {
   title string
   author string
}

func main() {
	var book1 Books
	Book1.title = "Go 語言入門"
	Book1.author = "mars.hao"	
}
登入後複製

3.4.3 結構體指標

關於結構體指標的定義和申明同樣可以套用前文中講到的指標的相關定義,從而使用一個指標變數存放一個結構體變數的地址。

定義一個結構體變數的語法:var struct_pointer *Books

這種指標變數的初始化和上文指標部分的初始化方式相同struct_pointer = &Book1,但是和c語言中有所不同,使用結構體指標存取結構體成員仍然使用.操作符。格式如下:struct_pointer.title

3.5 字串

一個字串是一個不可改變的位元組序列,字串通常是用來包含人類可讀的文字資料。和陣列不同的是,字串的元素不可修改,是一個唯讀的位元組陣列。每個字串的長度雖然也是固定的,但是字串的長度並不是字串型別的一部分。

3.5.1 字串定義和初始化

Go語言字串的底層結構在reflect.StringHeader中定義,具體如下:

type StringHeader struct {
    Data uintptr
    Len  int
}
登入後複製

也就是說字串結構由兩個資訊組成:第一個是字串指向的底層位元組陣列,第二個是字串的位元組的長度。

字串其實是一個結構體,因此字串的賦值操作也就是reflect.StringHeader結構體的複製過程,並不會涉及底層位元組陣列的複製,所以我們也可以將字串陣列看作一個結構體陣列。

字串和陣列類似,內建的len函數返回字串的長度。

3.5.2 字串UTF8編碼

根據Go語言規範,Go語言的原始檔都是採用UTF8編碼。因此,Go原始檔中出現的字串面值常數一般也是UTF8編碼的(對於跳脫字元,則沒有這個限制)。提到Go字串時,我們一般都會假設字串對應的是一個合法的UTF8編碼的字元序列。

Go語言的字串中可以存放任意的二進位制位元組序列,而且即使是UTF8字元序列也可能會遇到壞的編碼。如果遇到一個錯誤的UTF8編碼輸入,將生成一個特別的Unicode字元‘\uFFFD’,這個字元在不同的軟體中的顯示效果可能不太一樣,在印刷中這個符號通常是一個黑色六角形或鑽石形狀,裡面包含一個白色的問號‘�’。

下面的字串中,我們故意損壞了第一字元的第二和第三位元組,因此第一字元將會列印為「�」,第二和第三位元組則被忽略;後面的「abc」依然可以正常解碼列印(錯誤編碼不會向後擴散是UTF8編碼的優秀特性之一)。程式碼如下:

fmt.Println("\xe4\x00\x00\xe7\x95\x8cabc") // �界abc
登入後複製

不過在for range迭代這個含有損壞的UTF8字串時,第一字元的第二和第三位元組依然會被單獨迭代到,不過此時迭代的值是損壞後的0:

// 0 65533  // \uFFFD, 對應 �
// 1 0      // 空字元
// 2 0      // 空字元
// 3 30028  // 界
// 6 97     // a
// 7 98     // b
// 8 99     // c
登入後複製

3.5.3 字串的強制型別轉換

在上文中我們知道原始碼往往會採用UTF8編碼,如果不想解碼UTF8字串,想直接遍歷原始的位元組碼:

  • 可以將字串強制轉為[]byte位元組序列後再行遍歷(這裡的轉換一般不會產生執行時開銷):

  • 採用傳統的下標方式遍歷字串的位元組陣列

除此以外,字串相關的強制型別轉換主要涉及到[]byte和[]rune兩種型別。每個轉換都可能隱含重新分配記憶體的代價,最壞的情況下它們的運算時間複雜度都是O(n)。

不過字串和[]rune的轉換要更為特殊一些,因為一般這種強制型別轉換要求兩個型別的底層記憶體結構要儘量一致,顯然它們底層對應的[]byte和[]int32型別是完全不同的內部佈局,因此這種轉換可能隱含重新分配記憶體的操作。

3.6 slice

簡單地說,切片就是一種簡化版的動態陣列。因為動態陣列的長度不固定,切片的長度自然也就不能是型別的組成部分了。陣列雖然有適用它們的地方,但是陣列的型別和操作都不夠靈活,而切片則使用得相當廣泛。

切片高效操作的要點是要降低記憶體分配的次數,儘量保證append操作(在後續的插入和刪除操作中都涉及到這個函數)不會超出cap的容量,降低觸發記憶體分配的次數和每次分配記憶體大小。

3.6.1 slice定義

我們先看看切片的結構定義,reflect.SliceHeader:

type SliceHeader struct {
    Data uintptr   // 指向底層的的陣列指標
    Len  int	   // 切片長度
    Cap  int	   // 切片最大長度
}
登入後複製

和陣列一樣,內建的len函數返回切片中有效元素的長度,內建的cap函數返回切片容量大小,容量必須大於或等於切片的長度。

切片可以和nil進行比較,只有當切片底層資料指標為空時切片本身為nil,這時候切片的長度和容量資訊將是無效的。如果有切片的底層資料指標為空,但是長度和容量不為0的情況,那麼說明切片本身已經被損壞了

只要是切片的底層資料指標、長度和容量沒有發生變化的話,對切片的遍歷、元素的讀取和修改都和陣列是一樣的。在對切片本身賦值或引數傳遞時,和陣列指標的操作方式類似,只是複製切片頭資訊(reflect.SliceHeader),並不會複製底層的資料。對於型別,和陣列的最大不同是,切片的型別和長度資訊無關,只要是相同型別元素構成的切片均對應相同的切片型別。

當我們想定義宣告一個切片時可以如下:

在對切片本身賦值或引數傳遞時,和陣列指標的操作方式類似,只是複製切片頭資訊·(reflect.SliceHeader),並不會複製底層的資料。對於型別,和陣列的最大不同是,切片的型別和長度資訊無關,只要是相同型別元素構成的切片均對應相同的切片型別

3.6.2 新增元素

append() :內建的泛型函數,可以向切片中增加元素。

  • 在切片尾部追加N個元素

var a []int
a = append(a, 1)               // 追加1個元素
a = append(a, 1, 2, 3)         // 追加多個元素, 手寫解包方式
a = append(a, []int{1,2,3}...) // 追加一個切片, 切片需要解包
登入後複製

注意:尾部新增在容量不足的條件下需要重新分配記憶體,可能導致巨大的記憶體分配和複製資料代價。即使容量足夠,依然需要用append函數的返回值來更新切片本身,因為新切片的長度已經發生了變化。

  • 在切片開頭位置新增元素

var a = []int{1,2,3}
a = append([]int{0}, a...)        // 在開頭位置新增1個元素
a = append([]int{-3,-2,-1}, a...) // 在開頭新增1個切片
登入後複製

注意:在開頭一般都會導致記憶體的重新分配,而且會導致已有的元素全部複製1次。因此,從切片的開頭新增元素的效能一般要比從尾部追加元素的效能差很多。

  • append鏈式操作

var a []int
a = append(a[:i], append([]int{x}, a[i:]...)...)     // 在第i個位置插入x
a = append(a[:i], append([]int{1,2,3}, a[i:]...)...) // 在第i個位置插入切片
登入後複製

每個新增操作中的第二個append呼叫都會建立一個臨時切片,並將a[i:]的內容複製到新建立的切片中,然後將臨時建立的切片再追加到a[:i]。

  • append和copy組合

a = append(a, 0)     // 切片擴充套件1個空間
copy(a[i+1:], a[i:]) // a[i:]向後移動1個位置
a[i] = x             // 設定新新增的元素
登入後複製

第三個操作中會建立一個臨時物件,我們可以借用copy函數避免這個操作,這種方式操作語句雖然冗長了一點,但是相比前面的方法,可以減少中間建立的臨時切片。

3.6.3 刪除元素

根據要刪除元素的位置有三種情況:

1、從開頭位置刪除;

  • 直接行動資料指標,程式碼如下:
a = []int{1, 2, 3, ...}
a = a[1:]                       // 刪除開頭1個元素
a = a[N:]                       // 刪除開頭N個元素
登入後複製
  • 將後面的資料向開頭移動,使用append原地完成(所謂原地完成是指在原有的切片資料對應的記憶體區間內完成,不會導致記憶體空間結構的變化)
a = []int{1, 2, 3, ...}
a = append(a[:0], a[1:]...) // 刪除開頭1個元素
a = append(a[:0], a[N:]...) // 刪除開頭N個元素
登入後複製
  • 使用copy將後續資料向前移動,程式碼如下:
a = []int{1, 2, 3}
a = a[:copy(a, a[1:])] // 刪除開頭1個元素
a = a[:copy(a, a[N:])] // 刪除開頭N個元素
登入後複製

2、從中間位置刪除;
對於刪除中間的元素,需要對剩餘的元素進行一次整體挪動,同樣可以用append或copy原地完成:

  • append刪除操作如下:
a = []int{1, 2, 3, ...}
a = append(a[:i], a[i+1], ...)
a = append(a[:i], a[i+N:], ...)
登入後複製
  • copy刪除操作如下:
a = []int{1, 2, 3}
a = a[:copy(a[:i], a[i+1:])] // 刪除中間1個元素
a = a[:copy(a[:i], a[i+N:])] // 刪除中間N個元素
登入後複製

3、從尾部刪除。

程式碼如下所示:

a = []int{1, 2, 3, ...}

a = a[:len(a)-1]   // 刪除尾部1個元素
a = a[:len(a)-N]   // 刪除尾部N個元素
登入後複製

刪除切片尾部的元素是最快的

3.7 函數

為完成某一功能的程式指令(語句)的集合,稱為函數。

3.7.1 函數分類

在Go語言中,函數是第一類物件,我們可以將函數保持到變數中。函數主要有具名匿名之分,包級函數一般都是具名函數,具名函數是匿名函數的一種特例,當匿名函數參照了外部作用域中的變數時就成了閉包函數,閉包函數是函數語言程式設計語言的核心。

舉例程式碼如下:

  • 具名函數:就和c語言中的普通函數意義相同,具有函數名、返回值以及函數引數的函數。

func Add(a, b int) int {
    return a+b
}
登入後複製
  • 匿名函數:指不需要定義函數名的一種函數實現方式,它由一個不帶函數名的函數宣告和函數體組成。

var Add = func(a, b int) int {
    return a+b
}
登入後複製

解釋幾個名詞如下:

  1. 閉包函數:返回為函數物件,不僅僅是一個函數物件,在該函數外還包裹了一層作用域,這使得,該函數無論在何處呼叫,優先使用自己外層包裹的作用域。
  2. 一級物件:支援閉包的多數語言都將函數作為第一級物件,就是說函數可以儲存到變數中作為引數傳遞給其他函數,最重要的是能夠被函數動態建立和返回。
  3. 包:go的每一個檔案都是屬於一個包的,也就是說go是以包的形式來管理檔案和專案目錄結構的。

3.7.2 函數宣告和定義

Go 語言函數定義格式如下:

func fuction_name([parameter list])[return types]{
	函數體
}
登入後複製

解析
func函數由func開始宣告
function_name函數名稱
parameter list參數列
return_types返回型別
函數體函數定義的程式碼集合

3.7.3 函數傳參

Go語言中的函數可以有多個引數和多個返回值,引數和返回值都是以傳值的方式和被呼叫者交換資料。在語法上,函數還支援可變數量的引數,可變數量的引數必須是最後出現的引數,可變數量的引數其實是一個切片型別的引數。

當可變引數是一個空介面型別時,呼叫者是否解包可變引數會導致不同的結果,我們解釋一下解包的含義,程式碼如下:

func main(){
	var a = []int{1, 2, 3}
	Print(a...)   // 解包
	Print(a)	  // 未解包
}

func Print(a ...int{}) {
	fmt.Println(a...)
}
登入後複製

以上當傳入引數為a...時即是對切片a進行了解包,此時其實相當於直接呼叫Print(1,2,3)。當傳入引數直接為 a時等價於直接呼叫Print([]int{}{1,2,3})

3.7.4 函數返回值

不僅函數的引數可以有名字,也可以給函數的返回值命名。

舉例程式碼如下:

func Find(m map[int]int, key int)(value int, ok bool) {
	value,ok = m[key]
	return
}
登入後複製

如果返回值命名了,可以通過名字來修改返回值,也可以通過defer語句在return語句之後修改返回值,舉例程式碼如下:

func mian() {
	for i := 0 ; i<3; i++ {
		defer func() { println(i) }
	}
}

// 該函數最終的輸出為:
// 3
// 3
// 3
登入後複製

以上程式碼中如果沒有defer其實返回值就是0,1,2,但defer語句會在函數return之後才會執行,也就是或只有以上函數在執行結束return之後才會執行defer語句,而該函數return時的i值將會達到3,所以最終的defer語句執行printlin的輸出都是3。

defer語句延遲執行的其實是一個匿名函數,因為這個匿名函數捕獲了外部函數的區域性變數v,這種函數我們一般叫閉包。閉包對捕獲的外部變數並不是傳值方式存取,而是以參照的方式存取。

這種方式往往會帶來一些問題,修復方法為在每一輪迭代中都為defer函數提供一個獨有的變數,修改程式碼如下:

func main() {
    for i := 0; i < 3; i++ {
        i := i // 定義一個迴圈體內區域性變數i
        defer func(){ println(i) } ()
    }
}

func main() {
    for i := 0; i < 3; i++ {
        // 通過函數傳入i
        // defer 語句會馬上對呼叫引數求值
        // 不再捕獲,而是直接傳值
        defer func(i int){ println(i) } (i)
    }
}
登入後複製

3.7.5 遞迴呼叫

Go語言中,函數還可以直接或間接地呼叫自己,也就是支援遞迴呼叫。Go語言函數的遞迴呼叫深度邏輯上沒有限制,函數呼叫的棧是不會出現溢位錯誤的,因為Go語言執行時會根據需要動態地調整函數棧的大小。這部分的知識將會涉及goroutint和動態棧的相關知識,我們將會在之後的博文中向大家解釋。

它的語法和c很相似,格式如下:

func recursion() {
   recursion() /* 函數呼叫自身 */
}

func main() {
   recursion()
}
登入後複製

【相關推薦:Go視訊教學、】

以上就是go語言是什麼時候出現的的詳細內容,更多請關注TW511.COM其它相關文章!

<script type="text/javascript" src="https://sw.php.cn/hezuo/43cc2463da342d2af2696436bd2d05f4.html?bottom" ></script>