Go學習語法Golang
1.安裝
2.使用vscode編輯器安裝go外掛
3.go語法
_
是go的空白識別符號,忽視用的 結尾不需要”;”編譯自動加
package main //包名 mian包表示可獨立執行的程式 包名可以不和目錄名一致 每個目錄一個包
import "fmt" //匯入標準庫包 這個是目錄路徑 全域性 ./相對目錄 /根目錄查詢
/* 第二種匯入方法 多行註釋
import fm "fmt" // 別名匯入
import (
"fmt"
"os"
)
*/
//init特殊的函式,每個含有該函式的包都會首先執行這個函式
func init(){
}
//主執行函式
func main() {
fmt.Println("hello, world")
}
3.1型別
型別可以是基本型別,如:int、float、bool、string;結構化的(複合的),如:struct、array、slice、map、channel;只描述型別的行為的,如:interface。
結構化的型別沒有真正的值,它使用 nil 作為預設值。Go 語言中不存在型別繼承
一個函式可以擁有多返回值,返回型別之間需要使用逗號分割,並使用小括號 () 將它們括起來,如:
func FunctionName (a typea, b typeb) (t1 type1, t2 type2)
return var1, var2
3.2常量
常量的定義格式:const identifier [type] = value
const Pi = 3.14159
在 Go 語言中,你可以省略型別說明符 [type],因為編譯器可以根據變數的值來推斷其型別。
常量還可以用作列舉:
const (
Unknown = 0
Female = 1
Male = 2
)
在這個例子中,iota 可以被用作列舉值:
const (
a = iota
b = iota
c = iota
)
第一個 iota 等於 0,每當 iota 在新的一行被使用時,它的值都會自動加 1;所以 a=0, b=1, c=2 可以簡寫為如下形式:
const (
a = iota
b
c
)
在每遇到一個新的常量塊或單個常量宣告時, iota 都會重置為 0
3.3變數
宣告變數的一般形式是使用 var 關鍵字:var identifier type。
//這種因式分解關鍵字的寫法一般用於宣告全域性變數。
var (
a int
b bool
str string
)
當一個變數被宣告之後,系統自動賦予它該型別的零值:int 為 0,float 為 0.0,bool 為 false,string 為空字串,指標為 nil。記住,所有的記憶體在 Go 中都是經過初始化的。
變數的命名規則遵循駱駝命名法,即首個單詞小寫,每個新單詞的首字母大寫,例如:numShips 和 startDate。
但如果你的全域性變數希望能夠被外部包所使用,則需要將首個單詞的首字母也大寫
當你在函式體內宣告區域性變數時,應使用簡短宣告語法 :=,例如:a := 1
所有像 int、float、bool 和 string 這些基本型別都屬於值型別,使用這些型別的變數直接指向存在記憶體中的值:像陣列和結構這些複合型別也是值型別
當使用等號 = 將一個變數的值賦值給另一個變數時,如:j = i,實際上是在記憶體中將 i 的值進行了拷貝:你可以通過 &i 來獲取變數 i 的記憶體地址
在 Go 語言中,指標屬於引用型別,其它的引用型別還包括 slices,maps和 channel。被引用的變數會儲存在堆中,以便進行垃圾回收,且比棧擁有更大的記憶體空間。
簡短形式,使用 := 賦值操作符
這是使用變數的首選形式,但是它只能被用在函式體內,而不可以用於全域性變數的宣告與賦值。使用操作符 := 可以高效地建立一個新的變數,稱之為初始化宣告。
3.4基本型別和運算子
var b bool = true
Go 擁有以下複數型別:
complex64 (32 位實數和虛數)
complex128 (64 位實數和虛數)
複數使用 re+imI 來表示,其中 re 代表實數部分,im 代表虛數部分,I 代表根號負 1。
var c1 complex64 = 5 + 10i
fmt.Printf("The value is: %v", c1)
// 輸出: 5 + 10i
一些像遊戲或者統計學類的應用需要用到隨機數。rand 包實現了偽隨機數的生成。
類型別名
在 type TZ int 中,TZ 就是 int 型別的新名稱(用於表示程式中的時區),然後就可以使用 TZ 來操作 int 型別的資料。
實際上,類型別名得到的新型別並非和原型別完全相同,新型別不會擁有原型別所附帶的方法
字元型別
var ch byte = 65 或 var ch byte = '\x41'
3.5字串
字串的內容(純位元組)可以通過標準索引法來獲取,在中括號 [] 內寫入索引,索引從 0 開始計數:
- 字串 str 的第 1 個位元組:str[0]
- 第 i 個位元組:str[i - 1]
- 最後 1 個位元組:str[len(str)-1]
需要注意的是,這種轉換方案只對純 ASCII 碼的字串有效。
注意事項 獲取字串中某個位元組的地址的行為是非法的,例如:&str[i]。
在迴圈中使用加號 + 拼接字串並不是最高效的做法,更好的辦法是使用函式 strings.Join()
strings 和 strconv 包
HasPrefix 判斷字串 s 是否以 prefix 開頭:
strings.HasPrefix(s, prefix string) bool
Contains 判斷字串 s 是否包含 substr:
strings.Contains(s, substr string) bool
Index 返回字串 str 在字串 s 中的索引(str 的第一個字元的索引),-1 表示字串 s 不包含字串 str:
strings.Index(s, str string) int
Replace 用於將字串 str 中的前 n 個字串 old 替換為字串 new,並返回一個新的字串,如果 n = -1 則替換所有字串 old 為字串 new:
strings.Replace(str, old, new, n) string
strings.ToLower(s) string //換為相應的小寫字元
strings.TrimSpace(s)// 來剔除字串開頭和結尾的空白符號
strings.Split(s, sep)
用於自定義分割符號來對指定字串進行分割,同樣返回 slice。
Join 用於將元素型別為 string 的 slice 使用分割符號來拼接組成一個字串:
strings.Join(sl []string, sep string) string
3.6時間和日期
time.Now()
3.7指標
Go 語言的取地址符是 &,放到一個變數前使用就會返回相應變數的記憶體地址。
var i1 = 5
fmt.Printf("An integer: %d, it's location in memory: %p\n", i1, &i1)
var intP *int
intP = &i1
一個指標變數可以指向任何一個值的記憶體地址 它指向那個值的記憶體地址在 32 位機器上佔用 4 個位元組,在 64 位機器上佔用 8 個位元組
對於任何一個變數 var, 如下表達式都是正確的:var == *(&var)。
4.控制結構
- if-else 結構
- switch 結構
- select 結構,用於 channel 的選擇
4.1if-else 結構
if condition1 {
// do something
} else if condition2 {
// do something else
}else {
// catch-all or default
}
if initialization; condition {
// do something
}
if value := process(data); value > max {
...
}
4.2 多返回值
value, err := pack1.Function1(param1)
anInt, _ = strconv.Atoi(origStr)
4.3 switch結構
switch var1 {
case val1:
...
case val2,val3,val4:
case 0: // 空分支,只有當 i == 0 時才會進入分支
case 0: fallthrough //執行下一個分支的程式碼
...
default:
...
}
類似 if-else
switch {
case i < 0:
f1()
case i == 0:
f2()
case i > 0:
f3()
}
任何支援進行相等判斷的型別都可以作為測試表達式的條件,包括 int、string、指標等。
//變數 a 和 b 被平行初始化,然後作為判斷條件:
switch a, b := x[i], y[j]; {
case a < b: t = -1
case a == b: t = 0
case a > b: t = 1
}
4.4 for 結構
4.4.1基於計數器的迭代
for 初始化語句; 條件語句; 修飾語句 {}
//示例
for i := 0; i < 5; i++ {
fmt.Printf("This is the %d iteration\n", i)
}
特別注意,永遠不要在迴圈體內修改計數器,這在任何語言中都是非常差的實踐!
您還可以在迴圈中同時使用多個計數器:
for i, j := 0, N; i < j; i, j = i+1, j-1 {}
4.4.2基於條件判斷的迭代
for 結構的第二種形式是沒有頭部的條件判斷迭代(類似其它語言中的 while 迴圈),基本形式為:for 條件語句 {}。
4.4.3無限迴圈
條件語句是可以被省略的,如 i:=0; ; i++ 或 for { } 或 for ;; { }(;; 會在使用 gofmt 時被移除):這些迴圈的本質就是無限迴圈。最後一個形式也可以被改寫為 for true { },但一般情況下都會直接寫 for { }。
想要直接退出迴圈體,可以使用 break 語句或 return 語句直接返回
break 只是退出當前的迴圈體,而 return 語句提前對函式進行返回
無限迴圈的經典應用是伺服器,用於不斷等待和接受新的請求。
for t, err = p.Token(); err == nil; t, err = p.Token() {
...
}
4.4.4for-range 結構
這是 Go 特有的一種的迭代結構
語法上很類似其它語言中 foreach 語句
一般形式為:for ix, val := range coll { }。
一個字串是 Unicode 編碼的字元(或稱之為 rune)集合,因此您也可以用它迭代字串:
for pos, char := range str {
...
}
4.5Break 與 continue
break 語句退出當前迴圈。
關鍵字 continue 忽略剩餘的迴圈體而直接進入下一次迴圈的過程,但不是無條件執行下一次迴圈,執行之前依舊需要滿足迴圈的判斷條件。
另外,關鍵字 continue 只能被用於 for 迴圈中。
4.6標籤與 goto
for、switch 或 select 語句都可以配合標籤(label)形式的識別符號使用,即某一行第一個以冒號(:)結尾的單詞
(標籤的名稱是大小寫敏感的,為了提升可讀性,一般建議使用全部大寫字母)
LABEL1:
for i := 0; i <= 5; i++ {
for j := 0; j <= 5; j++ {
if j == 4 {
continue LABEL1
}
fmt.Printf("i is: %d, and j is: %d\n", i, j)
}
}
特別注意 使用標籤和 goto 語句是不被鼓勵的:它們會很快導致非常糟糕的程式設計,而且總有更加可讀的替代方案來實現相同的需求。
如果您必須使用 goto,應當只使用正序的標籤(標籤位於 goto 語句之後),但注意標籤和 goto 語句之間不能出現定義新變數的語句,否則會導致編譯失敗。
5函式
Go 裡面有三種類型的函式:
- 普通的帶有名字的函式
- 匿名函式或者lambda函式
- 方法(Methods)
假設 f1 需要 3 個引數 f1(a, b, c int),同時 f2 返回 3 個引數 f2(a, b int) (int, int, int),就可以這樣呼叫 f1:f1(f2(a, b))。
函式過載(function overloading)指的是可以編寫多個同名函式,只要它們擁有不同的形參與/或者不同的返回值,在 Go 裡面函式過載是不被允許的。這將導致一個編譯錯誤:
Go 語言不支援這項特性的主要原因是函式過載需要進行多餘的型別匹配影響效能;沒有過載意味著只是一個簡單的函式排程。所以你需要給不同的函式使用不同的名字,我們通常會根據函式的特徵對函式進行命名
如果需要申明一個在外部定義的函式,你只需要給出函式名與函式簽名,不需要給出函式體:
func flushICache(begin, end uintptr) // implemented externally
函式也可以以申明的方式被使用,作為一個函式型別,就像:
type binOp func(int, int) int
在這裡,不需要函式體 {}。
5.1函式引數與返回值
函式定義時,它的形參一般是有名字的,不過我們也可以定義沒有形參名的函式,只有相應的形參型別,就像這樣:func f(int, int, float64)。
沒有引數的函式通常被稱為 niladic 函式(niladic function),就像 main.main()。
按值傳遞(call by value) 按引用傳遞(call by reference)
Go 預設使用按值傳遞來傳遞引數,也就是傳遞引數的副本。函式接收引數副本之後,在使用變數的過程中可能對副本的值進行更改,但不會影響到原來的變數
在函式呼叫時,像切片(slice)、字典(map)、介面(interface)、通道(channel)這樣的引用型別都是預設使用引用傳遞(即使沒有顯式的指出指標)。
如果一個函式需要返回四到五個值,我們可以傳遞一個切片給函式(如果返回值具有相同型別)或者是傳遞一個結構體(如果返回值具有不同的型別)。因為傳遞一個指標允許直接修改變數的值,消耗也更少。
命名的返回值(named return variables)
命名返回值作為結果形參(result parameters)被初始化為相應型別的零值,當需要返回的時候,我們只需要一條簡單的不帶引數的return語句。需要注意的是,即使只有一個命名返回值,也需要使用 () 括起來
func getX2AndX3(input int) (int, int) {
return 2 * input, 3 * input
}
func getX2AndX3_2(input int) (x2 int, x3 int) {
x2 = 2 * input
x3 = 3 * input
// return x2, x3
return
}
儘量使用命名返回值:會使程式碼更清晰、更簡短,同時更加容易讀懂
空白符(blank identifier)
空白符用來匹配一些不需要的值,然後丟棄掉,
i1, _, f1 = ThreeValues()
5.2傳遞變長引數
如果函式的最後一個引數是採用 …type 的形式,那麼這個函式就可以處理一個變長的引數,這個長度可以為 0,這樣的函式稱為變參函式。
func myFunc(a, b, arg ...int) {}
如果引數被儲存在一個數組 arr 中,則可以通過 arr… 的形式來傳遞引數呼叫變參函式。
package main
import "fmt"
func main() {
x := min(1, 3, 2, 0)
fmt.Printf("The minimum is: %d\n", x)
arr := []int{7,9,3,5,1}
x = min(arr...)
fmt.Printf("The minimum in the array arr is: %d", x)
}
func min(a ...int) int {
if len(a)==0 {
return 0
}
min := a[0]
for _, v := range a {
if v < min {
min = v
}
}
return min
}
但是如果變長引數的型別並不是都相同的呢
1.使用結構
type Options struct {
par1 type1,
par2 type2,
...
}
2.使用空介面:
使用預設的空介面 interface{},這樣就可以接受任何型別的引數
5.3defer 和追蹤
關鍵字 defer 允許我們推遲到函式返回之前(或任意位置執行 return 語句之後)一刻才執行某個語句或函式(為什麼要在返回之後才執行這些語句?因為 return 語句同樣可以包含一些操作,而不是單純地返回某個值)。
關鍵字 defer 的用法類似於面向物件程式語言 Java 和 C# 的 finally 語句塊,它一般用於釋放某些已分配的資源。
當有多個 defer 行為被註冊時,它們會以逆序執行(類似棧,即後進先出)
func f() {
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i) //4 3 2 1 0
}
}
關鍵字 defer 允許我們進行一些函式執行完成後的收尾工作
1.關閉檔案流 defer file.Close()
2.解鎖一個加鎖的資源
mu.Lock()
defer mu.Unlock()
3.列印最終報告defer printFooter()
4.關閉資料庫連結 defer disconnectFromDB()
使用 defer 語句實現程式碼追蹤
package main
import "fmt"
func trace(s string) string {
fmt.Println("entering:", s)
return s
}
func un(s string) {
fmt.Println("leaving:", s)
}
func a() {
defer un(trace("a"))
fmt.Println("in a")
}
func b() {
defer un(trace("b"))
fmt.Println("in b")
a()
}
func main() {
b()
}
使用 defer 語句來記錄函式的引數與返回值
package main
import (
"io"
"log"
)
func func1(s string) (n int, err error) {
defer func() {
log.Printf("func1(%q) = %d, %v", s, n, err)
}()
return 7, io.EOF
}
func main() {
func1("Go")
}
5.4內建函式
Go 語言擁有一些不需要進行匯入操作就可以使用的內建函式
- close 用於管道通訊
- len、cap len 用於返回某個型別的長度或數量(字串、陣列、切片、map 和管道);cap 是容量的意思,用於返回某個型別的最大容量(只能用於切片和 map)
- new、make new 和 make 均是用於分配記憶體:new 用於值型別和使用者定義的型別,如自定義結構,make 用於內建引用型別(切片、map 和管道)。它們的用法就像是函式,但是將型別作為引數:new(type)、make(type)。new(T) 分配型別 T 的零值並返回其地址,也就是指向型別 T 的指標。它也可以被用於基本型別:v := new(int)。make(T) 返回型別 T 的初始化之後的值,因此它比 new 進行更多的工作 new() 是一個函式,不要忘記它的括號
- copy、append 用於複製和連線切片
- panic、recover 兩者均用於錯誤處理機制
- print、println 底層列印函式,在部署環境中建議使用 fmt 包
- complex、real imag 用於建立和操作複數
5.5遞迴函式
最經典的例子便是計算斐波那契數列,即前兩個數為1,從第三個數開始每個數均為前兩個數之和。
package main
import "fmt"
func main() {
result := 0
for i := 0; i <= 10; i++ {
result = fibonacci(i)
fmt.Printf("fibonacci(%d) is: %d\n", i, result)
}
}
func fibonacci(n int) (res int) {
if n <= 1 {
res = 1
} else {
res = fibonacci(n-1) + fibonacci(n-2)
}
return
}
5.6將函式作為引數
函式可以作為其它函式的引數進行傳遞,然後在其它函式內呼叫執行,一般稱之為回撥。
package main
import (
"fmt"
)
func main() {
callback(1, Add)
}
func Add(a, b int) {
fmt.Printf("The sum of %d and %d is: %d\n", a, b, a+b)
}
func callback(y int, f func(int, int)) {
f(y, 2) // this becomes Add(1, 2)
}
5.7 閉包
下面是一個計算從 1 到 1 百萬整數的總和的匿名函式:
func() {
sum := 0
for i := 1; i <= 1e6; i++ {
sum += i
}
}()
defer 語句和匿名函式
匿名函式同樣被稱之為閉包
計算函式執行時間
start := time.Now()
longCalculation()
end := time.Now()
delta := end.Sub(start)
fmt.Printf("longCalculation took this amount of time: %s\n", delta)
通過記憶體快取來提升效能
當在進行大量的計算時,提升效能最直接有效的一種方式就是避免重複計算。通過在記憶體中快取和重複利用相同計算的結果,稱之為記憶體快取。
5.8陣列與切片
陣列是具有相同 唯一型別 的一組已編號且長度固定的資料項序列(這是一種同構的資料結構)
陣列長度必須是一個常量表達式,並且必須是一個非負整數。陣列長度也是陣列型別的一部分,所以[5]int和[10]int是屬於不同型別的
。陣列的編譯時值初始化是按照陣列順序完成的
元素的數目,也稱為長度或者陣列大小必須是固定的並且在宣告該陣列時就給出(編譯時需要知道陣列長度以便分配記憶體);陣列長度最大為 2Gb。
宣告的格式是:
var identifier [len]type
2種方式遍歷
for i:=0; i < len(arr1); i++{
arr1[i] = ...
}
for i,_:= range arr1 {
...
}
Go 語言中的陣列是一種 值型別 也就是 =賦值就是拷貝
var arr1 = new([5]int) //指標型別
var arr2 [5]int //值型別
這樣的結果就是當把一個數組賦值給另一個時,需要在做一次陣列記憶體的拷貝操作。
5.8.1陣列常量
var arrAge = [5]int{18, 20, 15, 22, 16}
var arrLazy = [...]int{5, 6, 7, 8, 22}
//從技術上說它們其實變化成了切片
var arrKeyValue = [5]string{3: "Chris", 4: "Ron"}
//只有索引 3 和 4 被賦予實際的值,其他元素都被設定為空的字串 在這裡陣列長度同樣可以寫成 … 或者直接忽略。
幾何點(或者數學向量)是一個使用陣列的經典例子。為了簡化程式碼通常使用一個別名:
type Vector3D [3]float32
var vec Vector3D
將陣列傳遞給函式
把一個大陣列傳遞給函式會消耗很多記憶體。有兩種方法可以避免這種現象:
- 傳遞陣列的指標
- 使用陣列的切片
5.8.2切片
切片(slice)是對陣列一個連續片段的引用(該陣列我們稱之為相關陣列,通常是匿名的),所以切片是一個引用型別(因此更類似於 C/C++ 中的陣列型別,或者 Python 中的 list 型別)。這個片段可以是整個陣列,或者是由起始和終止索引標識的一些項的子集。需要注意的是,終止索引標識的項不包括在切片內.切片是一個 長度可變的陣列。
優點 因為切片是引用,所以它們不需要使用額外的記憶體並且比使用陣列更有效率,所以在 Go 程式碼中 切片比陣列更常用。
宣告切片的格式是: var identifier []type(不需要說明長度)。
一個切片在未初始化之前預設為 nil,長度為 0。
切片的初始化格式是:var slice1 []type = arr1[start:end]
如果某個人寫:var slice1 []type = arr1[:] 那麼 slice1 就等於完整的 arr1 陣列(所以這種表示方式是 arr1[0:len(arr1)] 的一種縮寫)。另外一種表述方式是:slice1 = &arr1。
arr1[2:] 和 arr1[2:len(arr1)] 相同,都包含了陣列從第三個到最後的所有元素。
arr1[:3] 和 arr1[0:3] 相同,包含了從第一個到第三個元素(不包括第三個)。
一個由數字 1、2、3 組成的切片可以這麼生成:s := [3]int{1,2,3}[:] 甚至更簡單的 s := []int{1,2,3}。
s2 := s[:] 是用切片組成的切片,擁有相同的元素,但是仍然指向相同的相關陣列。一個切片 s 可以這樣擴充套件到它的大小上限:s = s[:cap(s)],如果再擴大的話就會導致執行時錯誤
注意 絕對不要用指標指向 slice。切片本身已經是一個引用型別,所以它本身就是一個指標!!
將切片傳遞給函式var arr = [5]int{0, 1, 2, 3, 4}
sum(arr[:])
用 make() 建立一個切片
var slice1 []type = make([]type, len)
make 的使用方式是:func make([]T, len, cap),其中 cap 是可選引數。
下面兩種方法可以生成相同的切片:
make([]int, 50, 100)
new([100]int)[0:50]
new() 和 make() 的區別
- new(T) 為每個新的型別T分配一片記憶體,初始化為 0 並且返回型別為*T的記憶體地址:這種方法 返回一個指向型別為 T,值為 0 的地址的指標,它適用於值型別如陣列和結構體(參見第 10 章);它相當於 &T{}。
- make(T) 返回一個型別為 T 的初始值,它只適用於3種內建的引用型別:切片、map 和 channel。
換言之,new 函式分配記憶體,make 函式初始化
bytes 包
型別 []byte 的切片十分常見 bytes 包和字串包十分類似
Buffer 可以這樣定義:var buffer bytes.Buffer。
var r *bytes.Buffer = new(bytes.Buffer)
func NewBuffer(buf []byte) *Buffer
建立一個 Buffer 物件並且用 buf 初始化好;NewBuffer 最好用在從 buf 讀取的時候使用。
通過 buffer 串聯字串
var buffer bytes.Buffer
for {
if s, ok := getNextString(); ok { //method getNextString() not shown here
buffer.WriteString(s)
} else {
break
}
}
fmt.Print(buffer.String(), "\n")
這種實現方式比使用 += 要更節省記憶體和 CPU,尤其是要串聯的字串數目特別多的時候。
5.8.3For-range 結構
這種構建方法可以應用於陣列和切片:
for ix, value := range slice1 {
...
}
5.8.4切片重組(reslice)
slice1 := make([]type, start_length, capacity)
改變切片長度的過程稱之為切片重組 reslicing,做法如下:slice1 = slice1[0:end],其中 end 是新的末尾索引(即長度)。
5.8.5字串、陣列和切片的應用
- 從字串生成位元組切片
可以通過程式碼 len([]int32(s)) 來獲得字串中字元的數量,但使用 utf8.RuneCountInString(s) 效率會更高一點 - 獲取字串的某一部分
使用 substr := str[start:end] 可以從字串 str 獲取到從索引 start 開始到 end-1 位置的子字串。同樣的,str[start:] 則表示獲取從 start 開始到 len(str)-1 位置的子字串。而 str[:end] 表示獲取從 0 開始到 end-1 的子字串。 - 字串和切片的記憶體結構
在記憶體中,一個字串實際上是一個雙字結構,即一個指向實際資料的指標和記錄字串長度的整數 修改字串中的某個字元
Go 語言中的字串是不可變的
將切片 b 的元素追加到切片 a 之後:a = append(a, b…)
複製切片 a 的元素到新的切片 b 上:b = make([]T, len(a))
copy(b, a)
刪除位於索引 i 的元素:a = append(a[:i], a[i+1:]…)切除切片 a 中從索引 i 至 j 位置的元素:a = append(a[:i], a[j:]…)
- 為切片 a 擴充套件 j 個元素長度:a = append(a, make([]T, j)…)
- 在索引 i 的位置插入元素 x:a = append(a[:i], append([]T{x}, a[i:]…)…)
- 在索引 i 的位置插入長度為 j 的新切片:a = append(a[:i], -
- append(make([]T, j), a[i:]…)…)
- 在索引 i 的位置插入切片 b 的所有元素:a = append(a[:i], append(b, -a[i:]…)…)
- 取出位於切片 a 最末尾的元素 x:x, a = a[len(a)-1], a[:len(a)-1]
- 將元素 x 追加到切片 a:a = append(a, x)
###6 Map
map 是一種特殊的資料結構:一種元素對(pair)的無序集合,pair 的一個元素是 key,對應的另一個元素是 value,所以這個結構也稱為關聯陣列或字典。map 這種資料結構在其他程式語言中也稱為字典(Python)、hash 和 HashTable 等
6.1宣告、初始化和 make
map 是引用型別,可以使用如下宣告:
var map1 map[keytype]valuetype
var map1 map[string]int
map 可以用 {key1: val1, key2: val2} 的描述方法來初始化,就像陣列和結構體一樣。
map 是 引用型別 的: 記憶體用 make 方法來分配。
map 的初始化:var map1 = make(map[keytype]valuetype)。
或者簡寫為:map1 := make(map[keytype]valuetype)。
不要使用 new,永遠用 make 來構造 map
使用 func() int 作為值的 map:
package main
import "fmt"
func main() {
mf := map[int]func() int{
1: func() int { return 10 },
2: func() int { return 20 },
5: func() int { return 50 },
}
fmt.Println(mf)
}
輸出結果為:map[1:0x10903be0 5:0x10903ba0 2:0x10903bc0]: 整形都被對映到函式地址。
用切片作為 map 的值
mp1 := make(map[int][]int)
mp2 := make(map[int]*[]int)
6.2 測試鍵值對是否存在及刪除元素
val1, isPresent = map1[key1]
isPresent 返回一個 bool 值:如果 key1 存在於 map1,val1 就是 key1 對應的 value 值,並且 isPresent為true;如果 key1 不存在,val1 就是一個空值,並且 isPresent 會返回 false。
if _, ok := map1[key1]; ok {
// ...
}
從 map1 中刪除 key1: 直接 delete(map1, key1) 就可以。如果 key1 不存在,該操作不會產生錯誤。
6.3 for-range 的配套用法
可以使用 for 迴圈構造 map:
for key, value := range map1 {
...
}
如果只想獲取 key,你可以這麼使用:
for key := range map1 {
fmt.Printf("key is: %d\n", key)
}
6.4map 型別的切片
假設我們想獲取一個 map 型別的切片,我們必須使用兩次 make() 函式,第一次分配切片,第二次分配 切片中每個 map 元素
package main
import "fmt"
func main() {
// Version A:
items := make([]map[int]int, 5)
for i:= range items {
items[i] = make(map[int]int, 1)
items[i][1] = 2
}
fmt.Printf("Version A: Value of items: %v\n", items)
// Version B: NOT GOOD!
items2 := make([]map[int]int, 5)
for _, item := range items2 {
item = make(map[int]int, 1) // item is only a copy of the slice element.
item[1] = 2 // This 'item' will be lost on the next iteration.
}
fmt.Printf("Version B: Value of items: %v\n", items2)
}
map 預設是無序的,不管是按照 key 還是按照 value 預設都不排序
將 map 的鍵值對調
7.結構(struct)與方法(method)
結構體是複合型別(composite types),當需要定義一個型別,它由一系列屬性組成,每個屬性都有自己的型別和值的時候,就應該使用結構體
結構體也是值型別,因此可以通過 new 函式來建立
組成結構體型別的那些資料稱為 欄位(fields)。每個欄位都有一個型別和一個名字;在一個結構體中,欄位名字必須是唯一的。
7.1 結構體定義
type identifier struct {
field1 type1
field2 type2
...
}
type T struct {a, b int}
也是合法的語法,它更適用於簡單的結構體。
使用 new
var t *T = new(T)
如果需要可以把這條語句放在不同的行
宣告 var t T 也會給 t 分配記憶體,並零值化記憶體,但是這個時候 t 是型別T
無論變數是一個結構體型別還是一個結構體型別指標,都使用同樣的 選擇器符(selector-notation) 來引用結構體的欄位:
type myStruct struct { i int }
var v myStruct // v是結構體型別變數
var p *myStruct // p是指向一個結構體型別變數的指標
v.i
p.i
初始化一個結構體例項(一個結構體字面量:struct-literal)的更簡短和慣用的方式如下:
ms := &struct1{10, 15.5, "Chris"}
// 此時ms的型別是 *struct1
var ms struct1
ms = struct1{10, 15.5, "Chris"}
&struct1{a, b, c} 是一種簡寫,底層仍然會呼叫 new (),
這裡值的順序必須按照欄位順序來寫。
表示式 new(Type) 和 &Type{} 是等價的
type Interval struct {
start int
end int
}
//初始化方式:
intr := Interval{0, 3} (A)
intr := Interval{end:5, start:1} (B)
intr := Interval{end:5} (C)
如果想知道結構體型別T的一個例項佔用了多少記憶體,可以使用:size := unsafe.Sizeof(T{})
7.2 map 和 struct vs new() 和 make()
現在為止我們已經見到了可以使用 make() 的三種類型中的其中兩個
slices / maps / channels
試圖 make() 一個結構體變數,會引發一個編譯錯誤
7.3帶標籤的結構體
結構體中的欄位除了有名字和型別外,還可以有一個可選的標籤(tag):它是一個附屬於欄位的字串,可以是文件或其他的重要標記
package main
import (
"fmt"
"reflect"
)
type TagType struct { // tags
field1 bool "An important answer"
field2 string "The name of the thing"
field3 int "How much there are"
}
func main() {
tt := TagType{true, "Barak Obama", 1}
for i := 0; i < 3; i++ {
refTag(tt, i)
}
}
func refTag(tt TagType, ix int) {
ttType := reflect.TypeOf(tt)
ixField := ttType.Field(ix)
fmt.Printf("%v\n", ixField.Tag)
}
7.4 匿名欄位和內嵌結構體
結構體可以包含一個或多個 匿名(或內嵌)欄位,即這些欄位沒有顯式的名字,只有欄位的型別是必須的,此時型別就是欄位的名字。
可以粗略地將這個和麵向物件語言中的繼承概念相比較,隨後將會看到它被用來模擬類似繼承的行為
package main
import "fmt"
type innerS struct {
in1 int
in2 int
}
type outerS struct {
b int
c float32
int // anonymous field
innerS //anonymous field
}
func main() {
outer := new(outerS)
outer.b = 6
outer.c = 7.5
outer.int = 60
outer.in1 = 5
outer.in2 = 10
fmt.Printf("outer.b is: %d\n", outer.b)
fmt.Printf("outer.c is: %f\n", outer.c)
fmt.Printf("outer.int is: %d\n", outer.int)
fmt.Printf("outer.in1 is: %d\n", outer.in1)
fmt.Printf("outer.in2 is: %d\n", outer.in2)
// 使用結構體字面量
outer2 := outerS{6, 7.5, 60, innerS{5, 10}}
fmt.Println("outer2 is:", outer2)
}
在一個結構體中對於每一種資料型別只能有一個匿名欄位。
內嵌結構體
同樣地結構體也是一種資料型別,所以它也可以作為一個匿名欄位來使用,如同上面例子中那樣。
命名衝突
當兩個欄位擁有相同的名字(可能是繼承來的名字)時該怎麼辦呢?
- 外層名字會覆蓋內層名字(但是兩者的記憶體空間都保留),這提供了一種過載欄位或方法的方式;
- 如果相同的名字在同一級別出現了兩次,如果這個名字被程式使用了,將會引發一個錯誤(不使用沒關係)。沒有辦法來解決這種問題引起的二義性,必須由程式設計師自己修正。
7.5方法
在 Go 語言中,結構體就像是類的一種簡化形式,那麼面向物件程式設計師可能會問:類的方法在哪裡呢?在 Go 中有一個概念,它和方法有著同樣的名字,並且大體上意思相同:Go 方法是作用在接收者(receiver)上的一個函式,接收者是某種型別的變數。因此方法是一種特殊型別的函式。
一個型別加上它的方法等價於面向物件中的一個類。一個重要的區別是:在 Go 中,型別的程式碼和繫結在它上面的方法的程式碼可以不放置在一起,它們可以存在在不同的原始檔,唯一的要求是:它們必須是同一個包的。
型別 T(或 *T)上的所有方法的集合叫做型別 T(或 *T)的方法集。
不允許方法過載,即對於一個型別只能有一個給定名稱的方法
有同樣名字的方法可以在 2 個或多個不同的接收者型別上存在,比如在同一個包裡這麼做是允許的:
func (a *denseMatrix) Add(b Matrix) Matrix
func (a *sparseMatrix) Add(b Matrix) Matrix
別名型別不能有它原始型別上已經定義過的方法。
定義方法的一般格式如下:
func (recv receiver_type) methodName(parameter_list) (return_value_list) { ... }
如果 recv 一個指標,Go 會自動解引用。如果方法不需要使用 recv 的值,可以用 _ 替換它,
recv 就像是面嚮物件語言中的 this 或 self,
下面是非結構體型別上方法的例子
package main
import "fmt"
type IntVector []int
func (v IntVector) Sum() (s int) {
for _, x := range v {
s += x
}
return
}
func main() {
fmt.Println(IntVector{1, 2, 3}.Sum()) // 輸出是6
}
* 函式和方法的區別*
函式將變數作為引數:Function1(recv)
方法在變數上被呼叫:recv.Method1()
receiver_type 叫做 (接收者)基本型別,這個型別必須在和方法同樣的包中被宣告。
方法沒有和資料定義(結構體)混在一起:它們是正交的型別;表示(資料)和行為(方法)是獨立的。
指標或值作為接收者
鑑於效能的原因,recv 最常見的是一個指向 receiver_type 的指標(因為我們不想要一個例項的拷貝,如果按值呼叫的話就會是這樣),特別是在 receiver 型別是結構體時,就更是如此了。
在值和指標上呼叫方法:
可以有連線到型別的方法,也可以有連線到型別指標的方法。
但是這沒關係:對於型別 T,如果在 *T 上存在方法 Meth(),並且 t 是這個型別的變數,那麼 t.Meth() 會被自動轉換為 (&t).Meth()。
指標方法和值方法都可以在指標或非指標上被呼叫
7.6方法和未匯出欄位
提供 getter 和 setter 方法。對於 setter 方法使用 Set 字首,對於 getter 方法只使用成員名。
package person
type Person struct {
firstName string
lastName string
}
func (p *Person) FirstName() string {
return p.firstName
}
func (p *Person) SetFirstName(newName string) {
p.firstName = newName
}
可以覆寫方法(像欄位一樣):和內嵌型別方法具有同樣名字的外層型別的方法會覆寫內嵌型別對應的方法。
package main
import (
"fmt"
"math"
)
type Point struct {
x, y float64
}
func (p *Point) Abs() float64 {
return math.Sqrt(p.x*p.x + p.y*p.y)
}
type NamedPoint struct {
Point
name string
}
func (n *NamedPoint) Abs() float64 {
return n.Point.Abs() * 100.
}
func main() {
n := &NamedPoint{Point{3, 4}, "Pythagoras"}
fmt.Println(n.Abs()) // 列印5
}
因為一個結構體可以嵌入多個匿名型別,所以實際上我們可以有一個簡單版本的多重繼承
結構體內嵌和自己在同一個包中的結構體時,可以彼此訪問對方所有的欄位和方法。
7.7多重繼承
通過在型別中嵌入所有必要的父型別,可以很簡單的實現多重繼承。
package main
import (
"fmt"
)
type Camera struct{}
func (c *Camera) TakeAPicture() string {
return "Click"
}
type Phone struct{}
func (p *Phone) Call() string {
return "Ring Ring"
}
type CameraPhone struct {
Camera
Phone
}
func main() {
cp := new(CameraPhone)
fmt.Println("Our new CameraPhone exhibits multiple behaviors...")
fmt.Println("It exhibits behavior of a Camera: ", cp.TakeAPicture())
fmt.Println("It works like a Phone too: ", cp.Call())
}
7.8垃圾回收和 SetFinalizer
通過呼叫 runtime.GC() 函式可以顯式的觸發 GC,但這隻在某些罕見的場景下才有用,比如當記憶體資源不足時呼叫 runtime.GC(),它會在此函式執行的點上立即釋放一大片記憶體,
如果想知道當前的記憶體狀態,可以使用:
// fmt.Printf(“%d\n”, runtime.MemStats.Alloc/1024)
// 此處程式碼在 Go 1.5.1下不再有效,更正為
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf(“%d Kb\n”, m.Alloc / 1024)
8.0介面(Interfaces)與反射(reflection)
介面定義了一組方法(方法集),但是這些方法不包含(實現)程式碼:它們沒有被實現(它們是抽象的)。接口裡也不能包含變數。
通過如下格式定義介面:
type Namer interface {
Method1(param_list) return_type
Method2(param_list) return_type
...
}
(按照約定,只包含一個方法的)介面的名字由方法名加 [e]r 字尾組成,例如 Printer、Reader、Writer、Logger、Converter 等等。還有一些不常用的方式(當字尾 er 不合適時),比如 Recoverable,此時介面名以 able 結尾,或者以 I 開頭(像 .NET 或 Java 中那樣)。
8.1型別斷言
一個介面型別的變數 varI 中可以包含任何型別的值,必須有一種方式來檢測它的 動態 型別,即執行時在變數中儲存的值的實際型別
v := varI.(T) // unchecked type assertion
varI 必須是一個介面變數,否則編譯器會報錯
更安全的方式是使用以下形式來進行型別斷言:
if v, ok := varI.(T); ok { // checked type assertion
Process(v)
return
}
8.2型別判斷:type-switch
switch t := areaIntf.(type) {
case *Square:
fmt.Printf("Type Square %T with value %v\n", t, t)
case *Circle:
fmt.Printf("Type Circle %T with value %v\n", t, t)
case nil:
fmt.Printf("nil value: nothing to check?\n")
default:
fmt.P