字節第三屆青訓營是後端專場,開課了,高高興興寫筆記啦
課上很詳細的講了 Go 的基本語法,以及再加上自己閱讀 Go 語言聖經的一些總結,得出了這一篇文章,感覺跟 JS 和 c/c++ 還是有很多共通之處的。
內容來源於:Go 語言聖經 以及 第三屆青訓營課程
課程源碼 wangkechun/go-by-example
Go 語言簡介及安裝#
什麼是 Go 語言#
- 高性能、高並發
- 豐富的標準庫
- 完善的工具鏈
- 靜態鏈接
- 快速編譯
- 跨平台
- 垃圾回收
總而言之,兼顧 c/c++ 的性能,並具有 python 等語言的簡潔、完善的標準庫
安裝#
- 訪問 https://go.dev/ ,點擊 Download ,下載對應平台安裝包,安裝即可
- 如果無法訪問上述網址,可以改為訪問 https://studygolang.com/dl 下載安裝
- 如果訪問 github 速度比較慢,建議配置 go mod proxy,參考 https://goproxy.cn/ 裡面的描述配置,下載第三方依賴包的速度可以大大加快
IDE 推薦#
- vscode 安裝 Go 插件
- GoLand JetBrains 系列的新 IDE,dddd
可以通過 Github 很方便的登錄體驗該課程的示例項目 Dashboard — Gitpod (真好,我哭死)
基礎數據類型#
整型#
與 c++ 中類似,整型分有符號和無符號類型,有符號整數
- int8、int16、int32 和 int64
- 對應 8 位、16 位、32 位、64 位大小的有符號整數
- uint8、uint16、uint32 和 uint64 則對應無符號整數
- 另外的還有兩種對應特定 CPU 平台機器字大小的有符號和無符號整數
int
和uint
,其中int
也是應用最廣泛的數值類型,這兩種類型都有同樣的大小: 32 或 64bit- 不同的編譯器即使在相同的硬體平台上可能產生不同的大小。
- Unicode 字符
rune
類型是和int32
等價的類型,通常用於表示一個 Unicode 碼點。這兩個名稱可以互換使用。 byte
是uint8
類型的等價類型,byte
類型一般用於強調數值是一個原始的數據而不是一個小的整數。uintptr
類型,沒有指定具體的 bit 大小但是足以容納指針。只有在底層編程時才需要,特別是 Go 語言和 C 語言函數庫或操作系統接口相交互的地方。我們將在第十三章的 unsafe 包相關部分看到類似的例子
可通過 Printf
函數的 %b
參數打印二進制格式的數字,用%d
、%o
或 %x
參數控制輸出的進制格式,這部分與 c 中的格式化輸出類似,
var x uint8 = 1<<1 | 1<<5
fmt.Printf("%08b\n", x) // "00100010", the set {1, 5}
o := 0666
fmt.Printf("%d %[1]o %#[1]o\n", o) // "438 666 0666"
x := int64(0xdeadbeef)
fmt.Printf("%d %[1]x %#[1]x %#[1]X\n", x)
// Output:
// 3735928559 deadbeef 0xdeadbeef 0XDEADBEEF
ascii := 'a'
unicode := '國'
newline := '\n'
fmt.Printf("%d %[1]c %[1]q\n", ascii) // "97 a 'a'"
fmt.Printf("%d %[1]c %[1]q\n", unicode) // "22269 國 '國'"
上面的例子中,一般情況下 Printf 格式化字符串包含多個%
參數時將會包含對應相同數量的額外操作數,但是 %
之後的[1]
副詞告訴Printf
函數再次使用第一個操作數。
%
後的#
副詞告訴Printf
在用%o
、%x
或%X
輸出時生成0
、0x
或0X
前綴。- 字符使用
%c
參數打印,或者使用%q
參數打印帶單引號的字符
內置的 len
函數返回一個有符號的int
,可以像下面例子那樣處理逆序循環。
medals := []string{"gold", "silver", "bronze"}
for i := len(medals) - 1; i >= 0; i-- {
fmt.Println(medals[i]) // "bronze", "silver", "gold"
}
浮點數#
Go 中的浮點型有 float32
和 float64
其範圍極限值可以在 math 包找到。
- 常量
math.MaxFloat32
表示float32
能表示的最大數值,大約是3.4e38
;對應的math.MaxFloat64
常量大約是1.8e308
。它們分別能表示的最小值近似為1.4e-45
和4.9e-324
。 - 使用
Printf
函數的%g
參數打印浮點數,將採用更緊湊的表示形式打印,並提供足夠的精度,但是對應表格的數據,使用%e
(帶指數)或%f
的形式打印可能更合適。所有的這三個打印形式都可以指定打印的寬度和控制打印精度。
for x := 0; x < 8; x++ {
fmt.Printf("x = %d e^x = %8.3f\n", x, math.Exp(float64(x)))
}
// x = 0 e^x = 1.000
// x = 1 e^x = 2.718
// x = 2 e^x = 7.389
// x = 3 e^x = 20.086
// x = 4 e^x = 54.598
// x = 5 e^x = 148.413
// x = 6 e^x = 403.429
// x = 7 e^x = 1096.633
math 包中除了提供大量常用的數學函數外,還提供了 IEEE754 浮點數標準中定義的特殊值的創建和測試:正無窮大和負無窮大Inf -Inf
,分別用於表示太大溢出的數字和除零的結果;還有 NaN
非數,一般用於表示無效的除法操作結果,如 0/0 或 Sqrt (-1)
var z float64
fmt.Println(z, -z, 1/z, -1/z, z/z) // "0 -0 +Inf -Inf NaN"
- Go 中的
NaN
與 JS 中類似,跟任何數都是不相等的,包括其自身,可以用math.IsNaN
用於測試一個數是否是非數NaN
nan := math.NaN()
fmt.Println(nan == nan, nan < nan, nan > nan) // "false false false"
复数#
Go 語言提供了兩種精度的複數類型:complex64
和complex128
,分別對應float32
和float64
兩種浮點數精度。內置的 complex
函數用於構建複數,內建的 real
和 imag
函數分別返回複數的實部和虛部
var x complex128 = complex(1, 2) // 1+2i
var y complex128 = complex(3, 4) // 3+4i
fmt.Println(x*y) // "(-5+10i)"
fmt.Println(real(x*y)) // "-5"
fmt.Println(imag(x*y)) // "10"
如果一個浮點數面值或一個十進制整數面值後面跟著一個 i,例如3.141592i
或2i
,它將構成一個複數的虛部,複數的實部是 0:
fmt.Println(1i * 1i) // "(-1+0i)", i^2 = -1
一個複數常量可以正常加到另一個普通數值常量
fmt.Println(1i * 1i) // "(-1+0i)", i^2 = -1
math/cmplx 包提供了複數處理的許多函數,例如求複數的平方根函數和求幂函數。
fmt.Println(cmplx.Sqrt(-1)) // "(0+1i)"
布爾型#
true
or false
,這一點沒什麼好說的。
字符串#
Go 中的字符串類型string
是 不可變字符串,與 JS 一樣,與 c++ 不同。
不變性意味著如果兩個字符串共享相同的底層數據的話也是安全的,這使得複製任何長度的字符串代價是低廉的。同樣,一個字符串 s 和對應的子字符串切片 s [7:] 的操作也可以安全地共享相同的內存,因此字符串切片操作代價也是低廉的。在這兩種情況下都沒有必要分配新的內存。
字符串中的第 i
個字節並不一定是字符串的第 i
個字符,因為對於非 ASCII 字符的 UTF8 編碼會要兩個或多個字節。
s[i:j]
基於原始的 s
字符串的第 i
個字節開始到第 j
個字節(不包含 j
本身)生成一個新字符串。生成的新字符串將包含 j-i
個字節。
i
和j
都可以被忽略,當它們被忽略時將採用0
作為開始位置,採用len(s)
作為結束的位置。
fmt.Println(s[0:5]) // "hello"
fmt.Println(s[:5]) // "hello"
fmt.Println(s[7:]) // "world"
fmt.Println(s[:]) // "hello, world"
+
操作符將兩個字符串連接構造一個新字符串:
fmt.Println("goodbye" + s[5:]) // "goodbye, world"
字符串的比較是通過逐個字節比較完成的,比較結果是字符串自然編碼的順序。
Go 語言源文件總是用 UTF8 編碼,並且 Go 語言的文本字符串也以 UTF8 編碼的方式處理,因此我們可以將 Unicode 碼點也寫到字符串面值中。
一個原生的字符串面值形式如下,使用反引號代替雙引號。
const GoUsage = `Go is a tool for managing Go source code.
Usage:
go command [arguments]
...`
在原生的字符串面值中,沒有轉義操作;全部的內容都是字面的意思,包含退格和換行,因此一個程序中的原生字符串面值可能跨越多行
- 在原生字符串面值內部是無法直接寫・反引號的,可以用八進制或十六進制轉義或 +"`" 連接字符串常量完成)。
- 唯一的特殊處理是會刪除回車以保證在所有平台上的值都是一樣的,包括那些把回車也放入文本文件的系統
Windows 系統會把回車和換行一起放入文本文件中
以下是一些字符串方法
package main
import (
"fmt"
"strings"
)
func main() {
a := "hello"
fmt.Println(strings.Contains(a, "ll")) // true
fmt.Println(strings.Count(a, "l")) // 2
fmt.Println(strings.HasPrefix(a, "he")) // true
fmt.Println(strings.HasSuffix(a, "llo")) // true
fmt.Println(strings.Index(a, "ll")) // 2
fmt.Println(strings.Join([]string{"he", "llo"}, "-")) // he-llo
fmt.Println(strings.Repeat(a, 2)) // hellohello
fmt.Println(strings.Replace(a, "e", "E", -1)) // hEllo
fmt.Println(strings.Split("a-b-c", "-")) // [a b c]
fmt.Println(strings.ToLower(a)) // hello
fmt.Println(strings.ToUpper(a)) // HELLO
fmt.Println(len(a)) // 5
b := "你好"
fmt.Println(len(b)) // 6
}
在 go 語言裡面的話,可以很輕鬆地用
%v
來打印任意類型的變量,而不需要區分數字字符串,也可以用%+v
打印詳細結果,%#v
則更詳細。
package main
import "fmt"
type point struct {
x, y int
}
func main() {
s := "hello"
n := 123
p := point{1, 2}
fmt.Println(s, n) // hello 123
fmt.Println(p) // {1 2}
fmt.Printf("s=%v\n", s) // s=hello
fmt.Printf("n=%v\n", n) // n=123
fmt.Printf("p=%v\n", p) // p={1 2}
fmt.Printf("p=%+v\n", p) // p={x:1 y:2}
fmt.Printf("p=%#v\n", p) // p=main.point{x:1, y:2}
f := 3.141592653
fmt.Println(f) // 3.141592653
fmt.Printf("%.2f\n", f) // 3.14
}
字符串和數字轉換#
go 語言當中,關於字符串和數字類型之間的轉換都在 strconv
這個包下,這個包是 string convert 這兩個單詞的縮寫。可以用 ParseInt
或者 ParseFloat
來解析一個字符串。也可以用 Atoi 把一個十進制字符串轉成數字。可以用 Itoa
把數字轉成字符串。
- 如果輸入不合法,那麼這些函數都會返回 error 除了 Itoa
package main
import (
"fmt"
"strconv"
)
func main() {
f, _ := strconv.ParseFloat("1.234", 64)
fmt.Println(f) // 1.234
n, _ := strconv.ParseInt("111", 10, 64)
fmt.Println(n) // 111
n, _ = strconv.ParseInt("0x1000", 0, 64)
fmt.Println(n) // 4096
n2, _ := strconv.Atoi("123")
fmt.Println(n2) // 123
n2, err := strconv.Atoi("AAA")
fmt.Println(n2, err) // 0 strconv.Atoi: parsing "AAA": invalid syntax
n3 := strconv.Itoa(123) // 這個不返回error
fmt.Println(n3) // 123
}
常量#
同其他語言的常量一樣,常量的值不可修改,且必須被初始化,若批量聲明常量時其除第一個其他的初始化表達式可被省略,若省略則使用前面的常量表初始化表達式,如下:
const pi = 3.14159 // approximately; math.Pi is a better approximation
const (
e = 2.71828182845904523536028747135266249775724709369995957496696763
pi = 3.14159265358979323846264338327950288419716939937510582097494459
)
const (
a = 1
b
c = 2
d
)
iota
常量生成器#
類似 c/c++ 中的枚舉類型
Enum
!!
常量聲明可以使用iota
常量生成器初始化,它用於生成一組以相似規則初始化的常量,但是不用每行都寫一遍初始化表達式。在一個 const
聲明語句中,在第一個聲明的常量所在的行,iota
將會被置為 0
,然後在每一個有常量聲明的行加一。
type Weekday int
const (
Sunday Weekday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)
// Sunday 對應 0
// Monday 對應 1
// ....
// Saturday 對應 6
也可以結合複雜的表達式使用 itoa
,如下例:每個常量對應表達式1 << iota
,是連續的 2 的幂
type Flags uint
const (
FlagUp Flags = 1 << iota // is up
FlagBroadcast // supports broadcast access capability
FlagLoopback // is a loopback interface
FlagPointToPoint // belongs to a point-to-point link
FlagMulticast // supports multicast access capability
)
fmt.Println(FlagUp, FlagBroadcast, FlagLoopback, FlagPointToPoint, FlagMulticast)
// 1 2 4 8 16
無類型常量#
許多常量並沒有一個明確的基礎類型。Go 的編譯器為這些沒有明確基礎類型的數字常量提供比基礎類型更高精度的算術運算;你可以認為 至少有 256bit 的運算精度 。這裡有六種未明確類型的常量類型,分別是無類型的布爾型、無類型的整數、無類型的字符、無類型的浮點數、無類型的複數、無類型的字符串。
只有常量可以是無類型的。當一個無類型的常量被賦值給一個變量的時候,無類型的常量將會被隱式轉換為對應的類型,如果轉換合法的話。
- 對於沒有顯式類型的變量聲明(包括簡短變量聲明),常量的形式將隱式決定變量的默認類型,
- 無類型整數常量轉換為
int
,它的內存大小是不確定的,無類型浮點數和複數常量則轉換為內存大小明確的float64
和complex128
。
- 無類型整數常量轉換為
程序結構#
https://books.studygolang.com/gopl-zh/ch2/ch2.html
聲明與變量#
var#
一般語法如下
var 變量名 類型 = 表達式
類型省略則根據表達式自動推導,如果表達式為空,則用 零值 初始化該變量(因此在 Go 語言中不存在未初始化的變量)
類型 | 零值 |
---|---|
數值 | 0 |
布爾 | false |
字符串 | "" |
陣列或結構體等聚合類型 | nil |
可以在一個聲明語句中同時聲明一組變量,或用一組初始化表達式聲明並初始化一組變量。
var i, j, k int // int int int
var b, f, s = true, 2.3, "hello" // bool float64 string
一組變量也可以通過調用一個函數,由函數返回的多個返回值初始化:
var f, err = os.Open(name) // os.Open returns a file and an error
簡短變量聲明 :=
#
以名字 := 表達式
的形式聲明變量,變量的類型根據表達式來自動推導
- 因為其簡潔和靈活的特點,簡短變量聲明被廣泛用於大部分的局部變量的聲明和初始化。
- 而 var 形式的聲明語句往往是用於需要顯式指定變量類型的地方,或者因為變量稍後會被重新賦值而初始值無關緊要的地方。
i := 100 // int
i, j := 0, 1 // int int
var boiling float64 = 100 // a float64
var names []string
var err error
- 簡短變量聲明語句對在同級詞法域已經聲明過的變量只會進行賦值行為
- 如果變量是在外部詞法域聲明的,那麼簡短變量聲明語句將會在當前詞法域重新聲明一個新的變量
指針#
與 c 語言中類似,通過 &
操作符取址,通過 *
取值
x := 1
p := &x // p, of type *int, points to x
fmt.Println(*p) // "1"
*p = 2 // equivalent to x = 2
fmt.Println(x) // "2"
任何類型的指針的零值都是 nil
。
- 若
p
指向某個有效變量,那麼p != nil
測試為真。 - 當兩指針指向同一個變量或全部是
nil
時才相等。
new
函數#
new(T)
將創建一個 T
類型的匿名變量,初始化為 T類型的零值
,然後返回變量地址,返回的指針類型為 *T
。
- Go 語言中的
new
是個預定義的函數,不是關鍵字!所以可以重新定義。
p := new(int) // p, *int 類型, 指向匿名的 int 變量
fmt.Println(*p) // "0"
*p = 2 // 設置 int 匿名變量的值為 2
fmt.Println(*p) // "2"
自增 / 自減運算#
自增語句i++
給i
加 1;這和i += 1
以及i = i + 1
都是等價的。對應的還有i--
給i
減 1。它們是語句,而不像 C 系的其它語言那樣是表達式。
- 所以
j = i++
非法,而且 ++ 和 -- 都只能放在變量名後面,因此--i
也非法。
類型 type
#
類似於 c++ 中的 typeof 的加強版,形式如下
type 類型名 底層類型
如下,聲明了兩種類型:Celsius
和 Fahrenheit
分別對應不同的溫度單位。
- 底層數據類型決定其內部結構和表達方式
- 它們雖然有著相同的底層類型
float64
,但是它們是不同的數據類型,因此它們不可以被相互比較或混在一個表達式運算。 - 類型轉換不會改變值本身,但是會使它們的語義發生變化。
import "fmt"
type Celsius float64 // 攝氏溫度
type Fahrenheit float64 // 華氏溫度
const (
AbsoluteZeroC Celsius = -273.15 // 絕對零度
FreezingC Celsius = 0 // 結冰點溫度
BoilingC Celsius = 100 // 沸水溫度
)
func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }
func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }
比較運算符 == 和 < 也可以用來比較一個命名類型的變量和另一個有相同類型的變量,或有著相同底層類型的未命名類型的值之間做比較。但是如果兩個值有著不同的類型,則不能直接進行比較:
var c Celsius
var f Fahrenheit
fmt.Println(c == 0) // "true"
fmt.Println(f >= 0) // "true"
fmt.Println(c == f) // compile error: type mismatch
fmt.Println(c == Celsius(f)) // "true"! 類型轉換操作不會改變值
命名類型還可以為該類型的值定義新的行為。這些行為表示為一組關聯到該類型的函數集合,我們稱為類型的方法集 (在第六章會詳細講)
下面的聲明語句,Celsius 類型的參數 c 出現在了函數名的前面,表示聲明的是 Celsius 類型的一個名叫 String 的方法,該方法返回該類型對象 c 帶著 °C 溫度單位的字符串:
func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }
許多類型都會定義一個 String 方法,因為當使用 fmt 包的打印方法時,將會優先使用該類型對應的 String 方法返回的結果打印
c := FToC(212.0)
fmt.Println(c.String()) // "100°C"
fmt.Printf("%v\n", c) // "100°C"; no need to call String explicitly
fmt.Printf("%s\n", c) // "100°C"
fmt.Println(c) // "100°C"
fmt.Printf("%g\n", c) // "100"; does not call String
fmt.Println(float64(c)) // "100"; does not call String
循環 for
#
Go 中的循環沒有 while、do while 等,只有一種 for
循環
寫法如下:
for initialization; condition; post {
// zero or more statements
}
for 循環三個部分不需括號包圍。大括號強制要求,左大括號必須和post語句在同一行。
initialization
語句是可選的,在循環開始前執行。initalization
如果存在,必須是一條簡單語句(simple statement),即短變量聲明、自增語句、賦值語句或函數調用。condition
是一個布爾表達式(boolean expression),其值在每次循環迭代開始時計算。如果為true
則執行循環體語句。post
語句在每次循環體執行結束後執行,之後再次對condition
求值。condition
值為false
時,循環結束。
for 循環的這三個部分每個都可以省略,如果省略initialization
和post
,就是 while 循環,分號也可以省略,如果省略三個部分,則為永真循環,可通過 break
跳出:
i := 1
for {
fmt.Println("loop")
break
}
for j := 7; j < 9; j++ {
fmt.Println(j)
}
for n := 0; n < 5; n++ {
if n%2 == 0 {
continue
}
fmt.Println(n)
}
for i <= 3 {
fmt.Println(i)
i = i + 1
}
分支結構#
if else#
Go 中的 if
類似 python,沒有括號,但後面必須跟大括號
if 7%2 == 0 {
fmt.Println("7 is even")
} else {
fmt.Println("7 is odd")
}
if 8%4 == 0 {
fmt.Println("8 is divisible by 4")
}
if num := 9; num < 0 {
fmt.Println(num, "is negative")
} else if num < 10 {
fmt.Println(num, "has 1 digit")
} else {
fmt.Println(num, "has multiple digits")
}
switch#
go 語言裡面的 switch
分支結構類似 c++。但也有很多不同:
- switch 後面的那個變量名,也不要括號
- c++ 中的 switch case 如果不加
break
的話會然後會繼續往下跑完所有的 case, 在 go 語言裡面的話是不需要加break
的 - go 語言裡面的 switch 功能更強大,可以使用任意的變量類型,甚至可以用來取代任意的 if else 語句。
你可以在 switch 後面不加任何的變量,然後在 case 裡面寫條件分支。這樣代碼相比你用多個 if else 代碼邏輯會更為清晰。
package main
import (
"fmt"
"time"
)
func main() {
a := 2
switch a {
case 1:
fmt.Println("one")
case 2:
fmt.Println("two")
case 3:
fmt.Println("three")
case 4, 5:
fmt.Println("four or five")
default:
fmt.Println("other")
}
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("It's before noon")
default:
fmt.Println("It's after noon")
}
}
進程信息#
在 go 裡面,我們能夠用 os.argv
來得到程序執行的時候的指定的命令行參數。比如我們編譯的一個 二進制文件,command
。 後面接 abcd
來啟動,輸出就是 os.argv
會是個長度為 5
的 slice
, 第一個成員代表二進制自身的名字。我們可以用 so.getenv
來讀取環境變量。exec
package main
import (
"fmt"
"os"
"os/exec"
)
func main() {
// go run example/20-env/main.go a b c d
fmt.Println(os.Args) // [/var/folders/8p/n34xxfnx38dg8bv_x8l62t_m0000gn/T/go-build3406981276/b001/exe/main a b c d]
fmt.Println(os.Getenv("PATH")) // /usr/local/go/bin...
fmt.Println(os.Setenv("AA", "BB"))
buf, err := exec.Command("grep", "127.0.0.1", "/etc/hosts").CombinedOutput()
if err != nil {
panic(err)
}
fmt.Println(string(buf)) // 127.0.0.1 localhost
}
複合數據類型#
陣列#
陣列是一個由固定長度的特定類型元素組成的序列,一個陣列可以由零個或多個元素組成。因為陣列的長度是固定的,因此在 Go 語言中很少直接使用陣列。和陣列對應的類型是Slice
(切片),它是可以增長和收縮的動態序列,slice 功能也更靈活,但是要理解 slice 工作原理的話需要先理解陣列。
package main
import "fmt"
func main() {
var a [5]int
a[4] = 100
fmt.Println("get:", a[2])
fmt.Println("len:", len(a))
b := [5]int{1, 2, 3, 4, 5}
fmt.Println(b)
var twoD [2][3]int
for i := 0; i < 2; i++ {
for j := 0; j < 3; j++ {
twoD[i][j] = i + j
}
}
fmt.Println("2d: ", twoD)
}
切片 Slice
#
切片不同於陣列,可以任意更改長度,也有更多豐富的操作。
- 用
make
來創建一個切片,可以像陣列一樣去取值 - 使用
append
來追加元素。注意 append 的用法與 js 中的concat
相似,返回一個新陣列,把 append 的結果賦值為原陣列。 - slice 初始化的時候也可以動態的指定長度。
len(s)
- slice 擁有像 python 一樣的切片操作,比如
s[2:5]
代表取出第二個到第五個位置的元素,不包括第五個元素。不過不同於 python,這裡不支持負數索引
package main
import "fmt"
func main() {
s := make([]string, 3)
s[0] = "a"
s[1] = "b"
s[2] = "c"
fmt.Println("get:", s[2]) // c
fmt.Println("len:", len(s)) // 3
s = append(s, "d")
s = append(s, "e", "f")
fmt.Println(s) // [a b c d e f]
c := make([]string, len(s))
copy(c, s)
fmt.Println(c) // [a b c d e f]
fmt.Println(s[2:5]) // [c d e]
fmt.Println(s[:5]) // [a b c d e]
fmt.Println(s[2:]) // [c d e f]
good := []string{"g", "o", "o", "d"}
fmt.Println(good) // [g o o d]
}
Map#
map
是實際使用過程中最頻繁用到的數據結構。
- 可以用
make
來創建一個空map
,這裡會需要兩個類型,key
和value
的類型map[string]int
表示key
類型為string
、value
類型為int
map
的取值與插入類似 c++ 中 STL 的 map,可直接進行。m[key]
m[key] = value
- 可以用
delete
從裡面刪除鍵值對 - Go 中的
map
是完全無序的,遍歷的時候不會按照字母順序,也不會按照插入順序輸出,而是隨機順序
package main
import "fmt"
func main() {
m := make(map[string]int)
m["one"] = 1
m["two"] = 2
fmt.Println(m) // map[one:1 two:2]
fmt.Println(len(m)) // 2
fmt.Println(m["one"]) // 1
fmt.Println(m["unknow"]) // 0
r, ok := m["unknow"]
fmt.Println(r, ok) // 0 false
delete(m, "one")
m2 := map[string]int{"one": 1, "two": 2}
var m3 = map[string]int{"one": 1, "two": 2}
fmt.Println(m2, m3)
}
range#
對於一個 slice
或者一個 map
的話,我們可以用 range
來快速遍歷,這樣代碼能夠更加簡潔。 range 遍歷的時候,對於陣列會返回兩個值,第一個是索引,第二個是對應位置的值。如果我們不需要索引的話,我們可以用下劃線 _
來忽略。
Go 語言不允許使用無用的局部變量(local variables),因為這會導致編譯錯誤。用
空標識符
(blank identifier),即_
(也就是下劃線)。空標識符可用於在任何語法需要變量名但程序邏輯不需要的時候(如:在循環裡)丟棄不需要的循環索引,並保留元素值。
package main
import "fmt"
func main() {
nums := []int{2, 3, 4}
sum := 0
for i, num := range nums {
sum += num
if num == 2 {
fmt.Println("index:", i, "num:", num) // index: 0 num: 2
}
}
fmt.Println(sum) // 9
m := map[string]string{"a": "A", "b": "B"}
for k, v := range m {
fmt.Println(k, v) // b 8; a A
}
for k := range m {
fmt.Println("key: ", k) // key: a; key: b
}
for _, v := range m {
fmt.Println("value:", v) // value: A; value: B
}
}
結構體#
結構體的話是帶類型的字段的集合。比如這裡 user
結構包含了兩個字段,name
和 password
- 可以用結構體的名稱去初始化一個結構體變量,構造的時候需要傳入每個字段的初始值
- 也可以用鍵值對的方式指定初始值,這樣可以只對一部分字段進行初始化
- 同樣的結構體也支持指針,這樣能夠實現直接對於結構體的修改,可以在某些情況下避免一些大結構體的拷貝開銷
package main
import "fmt"
type user struct {
name string
password string
}
func main() {
a := user{name: "wang", password: "1024"}
b := user{"wang", "1024"}
c := user{name: "wang"}
c.password = "1024"
var d user
d.name = "wang"
d.password = "1024"
fmt.Println(a, b, c, d) // {wang 1024} {wang 1024} {wang 1024} {wang 1024}
fmt.Println(checkPassword(a, "haha")) // false
fmt.Println(checkPassword2(&a, "haha")) // false
}
func checkPassword(u user, password string) bool {
return u.password == password
}
func checkPassword2(u *user, password string) bool {
return u.password == password
}
JSON#
go 語言中的 JSON 操作非常簡單
- 對於一個已有的結構體,只要保證每個字段的第一個字母是大寫,也就是是公開字段。那麼這個結構體就能用
JSON.marshaler
去序列化,變成一個 JSON 的字符串。
JSON.marshaler
返回序列化值和 error,如下例
這樣默認序列化出來的字符串,是大寫字母開頭。可以在後面用 json tag 等語法來去修改輸出 JSON 結果裡面的字段名。
- 序列化之後的字符串可以用
JSON.unmarshaler
去反序列化到一個空的變量裡面。
package main
import (
"encoding/json"
"fmt"
)
type userInfo struct {
Name string
Age int `json:"age"`
Hobby []string
}
func main() {
a := userInfo{Name: "wang", Age: 18, Hobby: []string{"Golang", "TypeScript"}}
buf, err := json.Marshal(a)
if err != nil {
panic(err)
}
fmt.Println(buf) // [123 34 78 97...]
fmt.Println(string(buf)) // {"Name":"wang","age":18,"Hobby":["Golang","TypeScript"]}
buf, err = json.MarshalIndent(a, "", "\t")
if err != nil {
panic(err)
}
fmt.Println(string(buf))
var b userInfo
err = json.Unmarshal(buf, &b)
if err != nil {
panic(err)
}
fmt.Printf("%#v\n", b) // main.userInfo{Name:"wang", Age:18, Hobby:[]string{"Golang", "TypeScript"}}
}
時間處理#
go 語言最常用的就是 time.now()
來獲取當前時間,然後你也可以用 time.date
去構造一個帶時區的時間,有很多方法來獲取這個時間點的年月日小時分鐘秒,
- 可以用
Sub
方法對兩個時間進行減法,得到一個時間段。 - 時間段又可以得到它有多少小時,多少分鐘、多少秒。
- 在和某些系統交互的時候,我們經常會用到時間戳。那可以用
.UNIX
來獲取時間戳。time.format
time.parse
package main
import (
"fmt"
"time"
)
func main() {
now := time.Now()
fmt.Println(now) // 2022-05-07 13:12:03.7190528 +0800 CST m=+0.004990401
t := time.Date(2022, 5, 7, 13, 25, 36, 0, time.UTC)
t2 := time.Date(2022, 8, 12, 12, 30, 36, 0, time.UTC)
fmt.Println(t) // 2022-05-07 13:25:36 +0000 UTC
fmt.Println(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute()) // 2022 March 27 1 25
fmt.Println(t.Format("2006-01-02 15:04:05")) // 2022-05-07 13:25:36
diff := t2.Sub(t)
fmt.Println(diff) // 2327h5m0s
fmt.Println(diff.Minutes(), diff.Seconds()) // 139625 8.3775e+06
t3, err := time.Parse("2006-01-02 15:04:05", "2022-05-07 13:25:36")
if err != nil {
panic(err)
}
fmt.Println(t3 == t) // true
fmt.Println(now.Unix()) // 1651900531
}
函數#
Go 和其他很多語言不一樣的是,函數參數變量類型是後置的。Go 中的函數原生支持返回多個值。
- 在實際的業務邏輯代碼裡面幾乎所有的函數都返回兩個值,第一個是返回值,第二個值是一個錯誤信息。 如下例中的
exists
package main
import "fmt"
func add(a int, b int) int {
return a + b
}
func add2(a, b int) int {
return a + b
}
func exists(m map[string]string, k string) (v string, ok bool) {
v, ok = m[k]
return v, ok
}
func main() {
res := add(1, 2)
fmt.Println(res) // 3
v, ok := exists(map[string]string{"a": "A"}, "a")
fmt.Println(v, ok) // A True
}
錯誤處理#
go 中的錯誤處理就是使用一個單獨的返回值來傳遞錯誤信息
- 在函數返回值類型後面加一個
error
, 代表這個函數可能會返回錯誤。那么在函數實現的時候,return
需要同時return
兩個值 - 出現錯誤時,可以
return nil
和一個error
。如果沒有的話,那麼返回原本的結果和nil
。
package main
import (
"errors"
"fmt"
)
type user struct {
name string
password string
}
func findUser(users []user, name string) (v *user, err error) {
for _, u := range users {
if u.name == name {
return &u, nil
}
}
return nil, errors.New("not found")
}
func main() {
u, err := findUser([]user{{"wang", "1024"}}, "wang")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(u.name) // wang
if u, err := findUser([]user{{"wang", "1024"}}, "li"); err != nil {
fmt.Println(err) // not found
return
} else {
fmt.Println(u.name)
}
}
工具推薦#
在課堂中提到的幾個代碼生成工具
課後練習#
- 修改第一個例子猜謎遊戲裡面的最終代碼,使用 fmt.Scanf 來簡化代碼實現
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
maxNum := 100
rand.Seed(time.Now().UnixNano())
secretNumber := rand.Intn(maxNum)
// fmt.Println("The secret number is ", secretNumber)
fmt.Println("Please input your guess")
//reader := bufio.NewReader(os.Stdin)
for {
//input, err := reader.ReadString('\n')
var guess int
_, err := fmt.Scanf("%d", &guess)
fmt.Scanf("%*c") // 吃回車
if err != nil {
fmt.Println("An error occured while reading input. Please try again", err)
continue
}
//input = strings.TrimSuffix(input, "\n")
if err != nil {
fmt.Println("Invalid input. Please enter an integer value")
continue
}
fmt.Println("You guess is", guess)
if guess > secretNumber {
fmt.Println("Your guess is bigger than the secret number. Please try again")
} else if guess < secretNumber {
fmt.Println("Your guess is smaller than the secret number. Please try again")
} else {
fmt.Println("Correct, you Legend!")
break
}
}
}
- 修改第二個例子命令行詞典裡面的最終代碼,增加另一種翻譯引擎的支持
- 在上一步驟的基礎上,修改代碼實現並行請求兩個翻譯引擎來提高響應速度
總結及心得#
課上很詳細的講了 Go 的基本語法,以及再加上自己閱讀 Go 語言聖經的一些總結,得出了這一篇文章,感覺跟 JS 和 c/c++ 還是有很多共通之處的。
內容來源於:Go 語言聖經 以及 第三屆青訓營課程