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.Printf("Unexpected type %T\n", t)
}
可以用 type-switch 進行運行時類型分析,但是在 type-switch 不允許有 fallthrough 。
switch areaIntf.(type) {
case *Square:
// TODO
case *Circle:
// TODO
...
default:
// TODO
}
8.3測試一個值是否實現了某個接口
type Stringer interface {
String() string
}
if sv, ok := v.(Stringer); ok {
fmt.Printf("v implements String(): %s\n", sv.String()) // note: sv, not v
}
使用接口使代碼更具有普適性。
8.4使用方法集與接口
作用于變量上的方法實際上是不區分變量到底是指針還是值的
指針調用值類型方法時候會 指針會被自動解引用
在接口上調用方法時,必須有和方法定義時相同的接收者類型或者是可以從具體類型 P 直接可以辨識的:
- 指針方法可以通過指針調用
- 值方法可以通過值調用
- 接收者是值的方法可以通過指針調用,因為指針會首先被解引用
- 接收者是指針的方法不可以通過值調用,因為存儲在接口中的值沒有地址
Go 語言規范定義了接口方法集的調用規則:
- 類型 T 的可調用方法集包含接受者為 T 或 T 的所有方法集
- 類型 T 的可調用方法集包含接受者為 T 的所有方法
- 類型 T 的可調用方法集不包含接受者為 *T 的方法
8.5空接口
接口或者最小接口 不包含任何方法,它對實現不做任何要求:
type Any interface {}
任何其他類型都實現了空接口(它不僅僅像 Java/C# 中 Object 引用類型),any 或 Any 是空接口一個很好的別名或縮寫。
每個 interface {} 變量在內存中占據兩個字長:一個用來存儲它包含的類型,另一個用來存儲它包含的數據或者指向數據的指針。