GO語言基礎教程
GO語言基礎教程
文章目錄
- GO語言基礎教程
1、Hello World
1.1 初體驗
main.go
package main
import "fmt"
func main() {
fmt.Print("Hello World")
}
執行 go run main.go
編譯程式命令 為go build
1.2 fmt格式化引數介紹
原始碼介紹路徑
C:\Go\src\fmt\doc.go
中文文件參考 https://studygolang.com/pkgdoc fmt篇
通用:
%v 值的預設格式表示 %+v 類似%v,但輸出結構體時會新增欄位名 %#v 值的Go語法表示 %T 值的型別的Go語法表示 %% 百分號
布林值:
%t 單詞true或false
整數:
%b 表示為二進位制
%c 該值對應的unicode碼值
%d 表示為十進位制
%o 表示為八進位制
%q 該值對應的單引號括起來的go語法字元字面值,必要時會採用安全的轉義表示
%x 表示為十六進位制,使用a-f
%X 表示為十六進位制,使用A-F
%U 表示為Unicode格式:U+1234,等價於"U+%04X"
浮點數與複數的兩個組分:
%b 無小數部分、二進位制指數的科學計數法,如-123456p-78;參見strconv.FormatFloat
%e 科學計數法,如-1234.456e+78
%E 科學計數法,如-1234.456E+78
%f 有小數部分但無指數部分,如123.456
%F 等價於%f
%g 根據實際情況採用%e或%f格式(以獲得更簡潔、準確的輸出)
%G 根據實際情況採用%E或%F格式(以獲得更簡潔、準確的輸出)
字串和[]byte:
%s 直接輸出字串或者[]byte
%q 該值對應的雙引號括起來的go語法字串字面值,必要時會採用安全的轉義表示
%x 每個位元組用兩字元十六進位制數表示(使用a-f)
%X 每個位元組用兩字元十六進位制數表示(使用A-F)
指標:
%p 表示為十六進位制,並加上前導的0x
沒有%u。整數如果是無符號型別自然輸出也是無符號的。類似的,也沒有必要指定運算元的尺寸(int8,int64)。
寬度通過一個緊跟在百分號後面的十進位制數指定,如果未指定寬度,則表示值時除必需之外不作填充。精度通過(可選的)寬度後跟點號後跟的十進位制數指定。如果未指定精度,會使用預設精度;如果點號後沒有跟數字,表示精度為0。舉例如下:
%f: 預設寬度,預設精度
%9f 寬度9,預設精度
%.2f 預設寬度,精度2
%9.2f 寬度9,精度2
%9.f 寬度9,精度0
1.3 fmt例子
一定要記住的
%v %+v %#v %T %s %d %p
//https://blog.csdn.net/qq_34777600/article/details/81266453
package main
import "fmt"
import "os"
type point struct {
x, y int
}
func main() {
//Go 為常規 Go 值的格式化設計提供了多種列印方式。例如,這裡列印了 point 結構體的一個例項。
p := point{1, 2}
fmt.Printf("%v\n", p) // {1 2}
//如果值是一個結構體,%+v 的格式化輸出內容將包括結構體的欄位名。
fmt.Printf("%+v\n", p) // {x:1 y:2}
//%#v 形式則輸出這個值的 Go 語法表示。例如,值的執行原始碼片段。
fmt.Printf("%#v\n", p) // main.point{x:1, y:2}
//需要列印值的型別,使用 %T。
fmt.Printf("%T\n", p) // main.point
//格式化布林值是簡單的。
fmt.Printf("%t\n", true)
//格式化整形數有多種方式,使用 %d進行標準的十進位制格式化。
fmt.Printf("%d\n", 123)
//這個輸出二進位制表示形式。
fmt.Printf("%b\n", 14)
//這個輸出給定整數的對應字元。
fmt.Printf("%c\n", 33)
//%x 提供十六進位制編碼。
fmt.Printf("%x\n", 456)
//對於浮點型同樣有很多的格式化選項。使用 %f 進行最基本的十進位制格式化。
fmt.Printf("%f\n", 78.9)
//%e 和 %E 將浮點型格式化為(稍微有一點不同的)科學技科學記數法表示形式。
fmt.Printf("%e\n", 123400000.0)
fmt.Printf("%E\n", 123400000.0)
//使用 %s 進行基本的字串輸出。
fmt.Printf("%s\n", "\"string\"")
//像 Go 原始碼中那樣帶有雙引號的輸出,使用 %q。
fmt.Printf("%q\n", "\"string\"")
//和上面的整形數一樣,%x 輸出使用 base-16 編碼的字串,每個位元組使用 2 個字元表示。
fmt.Printf("%x\n", "hex this")
//要輸出一個指標的值,使用 %p。
fmt.Printf("%p\n", &p)
//當輸出數字的時候,你將經常想要控制輸出結果的寬度和精度,可以使用在 % 後面使用數字來控制輸出寬度。預設結果使用右對齊並且通過空格來填充空白部分。
fmt.Printf("|%6d|%6d|\n", 12, 345)
//你也可以指定浮點型的輸出寬度,同時也可以通過 寬度.精度 的語法來指定輸出的精度。
fmt.Printf("|%6.2f|%6.2f|\n", 1.2, 3.45)
//要最對齊,使用 - 標誌。
fmt.Printf("|%-6.2f|%-6.2f|\n", 1.2, 3.45)
//你也許也想控制字串輸出時的寬度,特別是要確保他們在類表格輸出時的對齊。這是基本的右對齊寬度表示。
fmt.Printf("|%6s|%6s|\n", "foo", "b")
//要左對齊,和數字一樣,使用 - 標誌。
fmt.Printf("|%-6s|%-6s|\n", "foo", "b")
//到目前為止,我們已經看過 Printf了,它通過 os.Stdout輸出格式化的字串。Sprintf 則格式化並返回一個字串而不帶任何輸出。
s := fmt.Sprintf("a %s", "string")
fmt.Println(s)
//你可以使用 Fprintf 來格式化並輸出到 io.Writers而不是 os.Stdout。
fmt.Fprintf(os.Stderr, "an %s\n", "error")
}
2、變數
Go 語言變數名由字母、數字、下劃線組成,其中首個字元不能為數字。
宣告變數的一般形式是使用 var 關鍵字:
-
宣告變數,再賦值
var name string name = "zhangsan"
-
簡潔賦值,宣告變數+賦值
name := "zhangsan"
在實際中,採用簡潔賦值可讀性比較好
3、基本型別
Go 的基本型別有
//bool
var ok bool = true
//string
var name string = "xxx"
//int int8 int16 int32 int64
//uint uint8 uint16 uint32 uint64 uintptr
var num int = 99
//byte 是 uint8 的別名
var numByte byte = 'c'
//rune 是 int32 的別名
//float32 float64
var f float32 = 12.1
//複數
//complex64 complex128
//陣列定義
var n [2]int
//陣列定義並賦值
var n = [2]int{1,2}
零值
沒有明確初始值的變數宣告會被賦予它們的零值:
零值是:
- 數值型別為
0
, - 布林型別為
false
, - 字串為
""
(空字串)。
3.1 字串型別
- 採用utf-8編碼,不會存在中文亂碼問題
- 雙引號表示,會識別轉義符
- 反單引號`表示,不轉移特定字元 ,結合fmt.Sprintf常用於複雜的字串拼接
宣告
var name string
//簡潔方式
name := "xxx"
package main
import "fmt"
func main() {
fmt.Println(`反單引號測試\t測試2\n,轉義符不起作用`)
user := fmt.Sprintf(`{"name":"zhangsan","age":%d}`,10)
fmt.Println(user)
//反單引號測試\t測試2\n,轉義符不起作用
//{"name":"zhangsan","age":10}
}
- strings標準庫使用C:\Go\src\strings\strings.go
//判斷s是否有後綴字串suffix
func HasSuffix(s, suffix string) bool
//判斷字串s是否包含子串substr
func Contains(s, substr string) bool
//子串sep在字串s中第一次出現的位置,不存在則返回-1。
func Index(s, sep string) int
//返回將所有字母都轉為對應的小寫版本的拷貝。
func ToLower(s string) string
//返回將所有字母都轉為對應的大寫版本的拷貝。
func ToUpper(s string) string
//返回count個s串聯的字串。
func Repeat(s string, count int) string
//返回將s中前n個不重疊old子串都替換為new的新字串,如果n<0會替換所有old子串。
func Replace(s, old, new string, n int) string
//返回將s前後端所有cutset包含的utf-8碼值都去掉的字串。
func Trim(s string, cutset string) string
//用去掉s中出現的sep的方式進行分割,會分割到結尾,並返回生成的所有片段組成的切片(每一個sep都會進行一次切割,即使兩個sep相鄰,也會進行兩次切割)。如果sep為空字元,Split會將s切分成每一個unicode碼值一個字串。
func Split(s, sep string) []string
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println(strings.HasSuffix("avatar.png","png"))
//redis裡獲取一個value裡面
value:="100:secret"
index := strings.Index(value,":")
if index>0 {
fmt.Println("secret:",value[index+1:])
}
fmt.Printf("%q\n", strings.Split("a,b,c", ","))
log("100",20,30)
}
func log(v ... interface{}){
format := ""
format+=strings.Repeat("%v ",len(v))
fmt.Printf(format,v...)
}
3.2 數字型別
// uint8 is the set of all unsigned 8-bit integers.
// Range: 0 through 255.
type uint8 uint8
// uint16 is the set of all unsigned 16-bit integers.
// Range: 0 through 65535.
type uint16 uint16
// uint32 is the set of all unsigned 32-bit integers.
// Range: 0 through 4294967295.
type uint32 uint32
// uint64 is the set of all unsigned 64-bit integers.
// Range: 0 through 18446744073709551615.
type uint64 uint64
// int8 is the set of all signed 8-bit integers.
// Range: -128 through 127.
type int8 int8
// int16 is the set of all signed 16-bit integers.
// Range: -32768 through 32767.
type int16 int16
// int32 is the set of all signed 32-bit integers.
// Range: -2147483648 through 2147483647.
type int32 int32
// int64 is the set of all signed 64-bit integers.
// Range: -9223372036854775808 through 9223372036854775807.
type int64 int64
// float32 is the set of all IEEE-754 32-bit floating-point numbers.
type float32 float32
// float64 is the set of all IEEE-754 64-bit floating-point numbers.
type float64 float64
// int is a signed integer type that is at least 32 bits in size. It is a
// distinct type, however, and not an alias for, say, int32.
// int在32作業系統為32位,最大值是2147483647;在64作業系統中為64位,最大值是9223372036854775807。注意,他不是int32
type int int
/*
package main
import "fmt"
func main() {
fmt.Println(int(1<<63 -1))
//9223372036854775807
}
*/
// uint is an unsigned integer type that is at least 32 bits in size. It is a
// distinct type, however, and not an alias for, say, uint32.
type uint uint
/*
package main
import "fmt"
func main() {
fmt.Println(uint(1<<64 -1))
//18446744073709551615
}
*/
開發過程中,我們經常用到時間戳,請定義為int64,不然又會埋下像“千年蟲”事件一樣的定時炸彈
2020-11-1 00:00:00 1604160000
2038-01-19 11:14:07 2147483647
3.2 基本型別間轉換
3.2.1 數字型別相互轉換
表示式 T(v)
將值 v
轉換為型別 T
。
一些關於數值的轉換:
var i int = 42
var f float64 = float64(i)
var u uint = uint(f)
或者,更加簡單的形式:
i := 42
f := float64(i)
u := uint(f)
與 C 不同的是,Go 在不同型別的項之間賦值時需要顯式轉換,沒有隱式轉換。
注意一點,大範圍轉小範圍,會出現精度丟失
package main
import "fmt"
//小端模式下
func main() {
//十六進位制值0x1234 儲存格式為0001 0010 0011 0100
//int8 只能儲存一個位元組,0011 0100 = 0x34
var num64 int64 = 0x1234
fmt.Printf("%x",int8(num64))
//列印16進製為34
}
補充一個知識點:網路中傳輸的資料統一是大端模式傳輸的,和兩段的系統無關,所以解析網路資料的時候要特別注意,大於一個位元組的數字型別,一定嚴格按照大端模式的資料解析
func tcpReadProto(rd *bufio.Reader, proto *Proto) ([]byte, error) {
var (
packLen int32
headerLen int16
err error
decodeBody []byte
)
// read
if err = binary.Read(rd, binary.BigEndian, &packLen); err != nil {
return nil, err
}
log.Debug("packLen: %d", packLen)
if err = binary.Read(rd, binary.BigEndian, &headerLen); err != nil {
return nil, err
}
log.Debug("headerLen: %d", headerLen)
...
}
3.2.2 數字和字串相互轉換
-
具體程式碼參考 C:\Go\src\strconv包
-
數字轉字串
//base 指定進位制,必須在2到36之間。
func FormatInt(i int64, base int) string
- 字串轉數字
//返回字串表示的整數值,接受正負號。
//base指定進位制(2到36),如果base為0,則會從字串前置判斷,"0x"是16進位制,"0"是8進位制,否則是10進位制;
//bitSize指定結果必須能無溢位賦值的整數型別,0、8、16、32、64 分別代表 int、int8、int16、int32、int64;返回的err是*NumErr型別的,如果語法有誤,err.Error = ErrSyntax;如果結果超出類型範圍err.Error = ErrRange。
func ParseInt(s string, base int, bitSize int) (i int64, err error)
- 簡潔寫法,int範圍內
//Itoa是FormatInt(i, 10) 的簡寫。
func Itoa(i int) string
//Atoi是ParseInt(s, 10, 0)的簡寫。
func Atoi(s string) (i int, err error)
- 例子
package main
import (
"fmt"
"log"
"strconv"
)
func main() {
str := strconv.FormatInt(111,10)
fmt.Printf("str is %s \n",str)
num,err := strconv.ParseInt("666",10,64)
if err !=nil {
log.Fatal(err.Error())
}
fmt.Printf("num is %d \n",num)
str2 := strconv.Itoa(99)
fmt.Printf("str2 is %s \n",str2)
num2,_ := strconv.Atoi("99")
fmt.Printf("num2 is %d \n",num2)
}
4 常量
- 常量在程式執行時,不可被修改。
- 常量中的資料型別只可以是布林型、數字型和字串型。
- 常量不能用
:=
語法宣告。 - 用const 定義
iota是一個預先宣告的識別符號,iota 在 const關鍵字出現時將被重置為 0,const 中每新增一行常量宣告將使 iota 計數一次,即下一行=前一行+1
package main
import "fmt"
func main() {
const (
Unknown = iota
Female
Male
)
fmt.Println(Unknown,Female,Male)
//0 1 2
}
5 語句
5.1 條件語句 if
if 表示式 {
/* 在表示式為 true 時執行 */
}
Go 的 if
語句與 for
迴圈類似,表示式外無需小括號 ( )
,而大括號 { }
則是必須的。
import (
"fmt"
)
func check() (string,bool) {
return "namespace",true
}
func main() {
if ns,ok := check(); ok {
fmt.Println(ns)
}
//注意ns的作用域在條件語句{}內,下列會編譯會報錯undefined: ns
//fmt.Println(ns)
}
5.2 條件語句 switch
//switch語句將expr表示式結果與可能的value值的列表進行匹配,然後執行響應程式碼
switch expr {
case vaule:
...
case vaule:
...
default:
...
}
switch
是編寫一連串 if - else
語句的簡便方法。它執行第一個值等於條件表示式的 case 語句。
Go 的 switch 語句類似於 C、C++、Java、JavaScript 和 PHP 中的,不過 Go 只執行選定的 case,而非之後所有的 case。 實際上,Go 自動提供了在這些語言中每個 case 後面所需的 break
語句。 除非以 fallthrough
語句結束,否則分支會自動終止。 Go 的另一點重要的不同在於 switch 的 case 無需為常量,且取值不必為整數。
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Print("Go runs on ")
os := runtime.GOOS
switch os {
case "darwin":
fmt.Println("OS X.")
case "linux":
fmt.Println("Linux.")
default:
fmt.Printf("%s.\n", os)
}
}
注意:switch 的 case 語句從上到下順次執行,直到匹配成功時停止。
package main
import "fmt"
func main() {
switch 3 {
case f1():
fmt.Println("execute 1 case statement")
case f2():
fmt.Println("execute 2 case statement")
case f3():
fmt.Println("execute 3 case statement")
case f4():
fmt.Println("execute 4 case statement")
default:
fmt.Println("execute default statement")
}
}
func f1() int {
fmt.Println("f1 function")
return 1
}
func f2() int {
fmt.Println("f2 function")
return 2
}
func f3() int {
fmt.Println("f3 function")
return 3
}
func f4() int {
fmt.Println("f4 function")
return 4
}
5.2.1 沒有條件的 switch
沒有條件的 switch 同 switch true
一樣。
這種形式能將一長串 if-then-else 寫得更加清晰。
package main
import (
"fmt"
)
func main() {
score:=55
if score>90 {
fmt.Println("優秀")
}else if score>80{
fmt.Println("良")
}else if score>70{
fmt.Println("中")
}else if score>60{
fmt.Println("及格")
}else {
fmt.Println("不及格")
}
switch {
case score>90:
fmt.Println("優秀")
case score>80:
fmt.Println("良")
case score>70:
fmt.Println("中")
case score>60:
fmt.Println("及格")
default:
fmt.Println("不及格")
}
}
5.2.2 fallthrough
使用 fallthrough 會強制執行後面的 case 語句,fallthrough 不會判斷下一條 case 的表示式結果是否為 true。
package main
import "fmt"
func main() {
switch {
case false:
fmt.Println("1、case 條件語句為 false")
fallthrough
case true:
fmt.Println("2、case 條件語句為 true")
fallthrough
case false:
fmt.Println("3、case 條件語句為 false")
fallthrough
case true:
fmt.Println("4、case 條件語句為 true")
case false:
fmt.Println("5、case 條件語句為 false")
fallthrough
default:
fmt.Println("6、預設 case")
}
}
5.3 迴圈語句
5.3.1 for
for 初始化; 控制條件語句; 控制條件賦值 {
//控制條件語句為true執行迴圈體
}
例子
package main
import "fmt"
func main() {
for i := 0; i < 10; i++ {
fmt.Println(i)
}
}
5.3.2 for range
for 迴圈的 range 格式可以對 slice、map、陣列、字串等進行迭代迴圈。格式如下:
for key, value := range arr {
//獲取key,value,然後對應操作
}
例子
package main
import "fmt"
func main() {
nums := []int{1, 2, 3, 5}
for key, value := range nums {
fmt.Printf("key=%d, value=%d\n", key, value)
}
}
/*
結果為
key=0, value=1
key=1, value=2
key=2, value=3
key=3, value=5
*/
5.3.3 continue
- 跳過當前迴圈的剩餘語句,然後繼續進行下一輪迴圈。
5.3.4 break
- 跳出迴圈,並開始執行迴圈之後的語句。
- break有坑
for迴圈在和switch語句,select語句連用時候,使用break不當,容易造成死迴圈
break是跳出最近的for迴圈,或者switch和select語句
可以實驗下面程式碼,break語句無法跳出迴圈
package main
import (
"fmt"
"time"
)
func main() {
for i := 1; ; i++ {
time.Sleep(time.Second)
switch i {
case 1:
fmt.Println("case 1,i=", i)
case 2:
fmt.Println("case 2,i=", i)
default:
fmt.Println("case default", i)
break
}
}
}
解決辦法:
- 可以加標籤,指定break出那一層。建議不要這樣做,程式碼不簡潔,可讀性差。
- 可以封裝switch語句在函式內,函式自帶作用域範圍,就不會引起這樣問題了。
package main
import (
"fmt"
"time"
)
func main() {
re:
for i := 1; ; i++ {
time.Sleep(time.Second)
switch i {
case 1:
fmt.Println("case 1,i=", i)
case 2:
fmt.Println("case 2,i=", i)
default:
fmt.Println("case default", i)
break re
}
}
}
package main
import (
"fmt"
"time"
)
func f(i int) {
switch i {
case 1:
fmt.Println("case 1,i=", i)
case 2:
fmt.Println("case 2,i=", i)
default:
fmt.Println("case default", i)
}
}
func main() {
for i := 1; ; i++ {
time.Sleep(time.Second)
f(i)
break
}
}
5.3.4 for range的坑
-
切記,不要對for range 的引用value的值
-
key和value只定義一次,在迴圈體內每迴圈一次做一次賦值。他們的地址是不變化的
package main
import (
"fmt"
)
func main() {
nums := []int{1, 3, 5, 7}
nums2 := make([]*int, 0)
for key, value := range nums {
nums2 = append(nums2, &value)
fmt.Printf("key=%d,keyp=%p, value=%d,valuep=%p\n", key, &key, value, &value)
}
for _, v := range nums2 {
fmt.Printf("p=%p,value=%d\n", v, *v)
}
}
go for range原始碼
https://github.com/golang/gofrontend/blob/master/go/statements.cc
For_range_statement::lower_range_slice
// The loop we generate:
// for_temp := range
// len_temp := len(for_temp)
// for index_temp = 0; index_temp < len_temp; index_temp++ {
// value_temp = for_temp[index_temp]
// index = index_temp
// value = value_temp
// original body
// }
6 指標
Go 擁有指標。指標儲存了值的記憶體地址。
型別 *T
是指向 T
型別值的指標。其零值為 nil
。
var p *int
&
操作符會生成一個指向其運算元的指標。
i := 42
p = &i
*
操作符表示指標指向的底層值。
fmt.Println(*p) // 通過指標 p 讀取 i
*p = 21 // 通過指標 p 設定 i
這也就是通常所說的“間接引用”或“重定向”。
與 C 不同,Go 沒有指標運算。
7 結構體
一個結構體(struct
)就是一組欄位(field)。
結構體欄位使用點號來訪問。
結構體欄位可以通過結構體指標來訪問。
package main
import "fmt"
type Vertex struct {
X, Y int
}
var (
v1 = Vertex{1, 2} // 建立一個 Vertex 型別的結構體
v2 = Vertex{X: 5, Y: 6} //指定成員賦值,與順序無關
v3 = Vertex{} // X:0 Y:0
p = &Vertex{9, 10} // 建立一個 *Vertex 型別的結構體(指標)
)
func main() {
fmt.Println("v1.x = ", v1.X)
fmt.Println("p.x = ", p.X)
fmt.Println(v1, v2, v3, p)
}
8 陣列
- 型別
[n]T
表示擁有n
個T
型別的值的陣列。 - 陣列的長度是其型別的一部分,陣列不能改變大小
- 表示式
var a [10]int //宣告
var f = [5]float32{1.0, 2.0, 3.1, 4.6, 20.1} //宣告並賦值
會將變數 a
宣告為擁有 10 個整數的陣列。
package main
import "fmt"
func main() {
var a [2]string
a[0] = "Hello"
a[1] = "World"
fmt.Println(a[0], a[1])
fmt.Println(a)
primes := [6]int{2, 3, 5, 7, 11, 13}
fmt.Println(primes)
}
9 切片
9.1 宣告
切片宣告類似於沒有長度的陣列文法。
這是一個數組文法:
//[3]int{1, 2, 3}
var bools = [3]int{1, 2, 3}
下面這樣則會建立一個和上面相同的陣列,然後構建一個引用了它的切片:
//[]int{1, 2, 3}
var s = []int{1, 2, 3}
每個陣列的大小都是固定的。而切片則為陣列元素提供動態大小的、靈活的視角。在實踐中,切片比陣列更常用。
型別 []T
表示一個元素型別為 T
的切片。
切片通過兩個下標來界定,即一個上界和一個下界,二者以冒號分隔:
a[0 : 3]
它會選擇一個半開區間,包括第一個元素,但排除最後一個元素。
以下表達式建立了一個切片,它包含 a
中下標從 1 到 3 的元素:
a[1:4]
例子:
package main
import "fmt"
func main() {
primes := [6]int{2, 3, 5, 7, 11, 13}
var s []int = primes[1:4]
fmt.Println(s)
}
nil是一個預先宣告的識別符號,表示指標、通道、函式、介面、對映或切片型別的零值。
9.2 切片就像陣列的引用
type slice struct {
array unsafe.Pointer
len int
cap int
}
切片並不儲存任何資料,它只是描述了底層陣列中的一段。
更改切片的元素會修改其底層陣列中對應的元素。
與它共享底層陣列的切片都會觀測到這些修改。
package main
import "fmt"
func main() {
names := [4]string{
"John",
"Paul",
"George",
"Ringo",
}
fmt.Println(names)
a := names[0:2]
b := names[1:3]
fmt.Println(a, b)
b[0] = "XXX"
fmt.Println(a, b)
fmt.Println(names)
}
9.3 切片的預設行為
在進行切片時,你可以利用它的預設行為來忽略上下界。
切片下界的預設值為 0
,上界則是該切片的長度。
對於陣列
var a [10]int
來說,以下切片是等價的:
a[0:10]
a[:10]
a[0:]
a[:]
package main
import "fmt"
func main() {
s := []int{2, 3, 5, 7, 11, 13}
s = s[1:4]
fmt.Println(s)
s = s[:2]
fmt.Println(s)
s = s[1:]
fmt.Println(s)
}
9.4 切片的長度與容量
切片擁有 長度 和 容量。
切片的長度就是它所包含的元素個數。
切片的容量是從它的第一個元素開始數,到其底層陣列元素末尾的個數。
切片 s
的長度和容量可通過表示式 len(s)
和 cap(s)
來獲取。
你可以通過重新切片來擴充套件一個切片,給它提供足夠的容量。試著修改示例程式中的切片操作,向外擴充套件它的容量,看看會發生什麼。
package main
import "fmt"
func main() {
s := []int{2, 3, 5, 7, 11, 13}
printSlice(s)
// 擷取切片使其長度為 0
s = s[:0]
printSlice(s)
// 拓展其長度
s = s[:4]
printSlice(s)
// 捨棄前兩個值
s = s[2:]
printSlice(s)
}
func printSlice(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
9.5 切片零值
切片的零值是 nil
。
nil 切片的長度和容量為 0 且沒有底層陣列。
package main
import "fmt"
func main() {
var s []int
fmt.Println(s, len(s), cap(s))
if s == nil {
fmt.Println("nil!")
}
}
9.6 用 make 建立切片
切片可以用內建函式 make
來建立,這也是你建立動態陣列的方式。
make
函式會分配一個元素為零值的陣列並返回一個引用了它的切片:
a := make([]int, 5) // len(a)=5
要指定它的容量,需向 make
傳入第三個引數:
b := make([]int, 0, 5) // len(b)=0, cap(b)=5
b = b[:cap(b)] // len(b)=5, cap(b)=5
b = b[1:] // len(b)=4, cap(b)=4
9.7 向切片追加元素
為切片追加新的元素是種常用的操作,為此 Go 提供了內建的 append
函式。內建函式的文件對此函式有詳細的介紹。
func append(s []T, vs ...T) []T
append
的第一個引數 s
是一個元素型別為 T
的切片,其餘型別為 T
的值將會追加到該切片的末尾。
append
的結果是一個包含原切片所有元素加上新新增元素的切片。
當 s
的底層陣列太小,不足以容納所有給定的值時,它就會分配一個更大的陣列。返回的切片會指向這個新分配的陣列。
package main
import "fmt"
func main() {
var s []int
printSlice(s)
// 新增一個空切片
s = append(s, 0)
printSlice(s)
// 這個切片會按需增長
s = append(s, 1)
printSlice(s)
// 可以一次性新增多個元素
s = append(s, 2, 3, 4)
printSlice(s)
}
func printSlice(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
9.8 切片擴容機制
- 當原切片長度小於1024時,新切片的容量會直接翻倍。而當原切片的容量大於等於1024時,會反覆地增加25%,直到新容量超過所需要的容量。
- 若新入元素大小超過了原有的容量,則新容量取兩者相加計算出來的最小cap值。
- 出於記憶體的高效利用考慮,還要進行記憶體對齊
src/runtime/slice.go
...
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
...
下列實驗觀察切片容量增長
package main
import "fmt"
var sCap int
func main() {
var s []int
for i := 0; i < 100000; i++ {
s = append(s, i)
printCapChange(s)
}
}
func printCapChange(s []int) {
oldCap := sCap
sCap = cap(s)
if oldCap != sCap && oldCap > 0 {
fmt.Printf("cap change %d %d \n", sCap, (sCap)*100/oldCap)
}
}
切片指向的陣列,每次擴容陣列會發生變化實驗
package main
import (
"fmt"
)
var sCap int
func main() {
var arr = [4]int{1, 2, 3, 4}
var s = arr[0:4]
fmt.Printf("陣列 %v ,%p\n", arr, &arr)
fmt.Printf("切片 %v ,%p\n", s, s)
for i := 0; i < 20; i++ {
s = append(s, i)
printCapChange(s)
}
s[0] = 999
fmt.Printf("陣列%v ,%p\n", arr, &arr)
fmt.Printf("切片底層的陣列已經改變 %v ,%p\n", s, s)
}
func printCapChange(s []int) {
oldCap := sCap
sCap = cap(s)
if oldCap != sCap && oldCap > 0 {
fmt.Printf("切片%p 容量改變為%d,擴大為原來%d%% \n", &s, sCap, (sCap)*100/oldCap)
}
}
9.9 切片引起的bug
s := arr[0:4]
//當你改變切片元素,底層的陣列或指向該陣列的其他切片,都可能會被修改,所以,如有修改操作,請使用copy函式
s[0] = 999
9.10 切片copy操作
//dst目標切片,目標切片的大小要和源切片大小一樣;src源切片
func copy(dst, src []Type) int
10 map
- 對應其他語言字典或hash表
- map是一種無序的鍵值對的集合
- map的零值是nil,可以用make來建立
- map併發讀寫不安全,不能同時併發讀寫,不能同時併發寫
- 如果只用來讀,是併發安全的
10.1 map操作
map宣告
//使用make宣告並初始化map
m := make(map[int]int)
//宣告並初始化map
var score = map[string]int{"zhangsan": 99, "lisi": 88, "wangwu": 59}
//簡潔宣告並初始化map
score1 := map[string]int{"zhangsan": 99, "lisi": 88, "wangwu": 59}
在map m
中插入或修改元素:
m[key] = elem
獲取元素:
elem = m[key]
刪除元素:
delete(m, key)
通過雙賦值檢測某個鍵是否存在:
elem, ok = m[key]
若 key
在 m
中,ok
為 true
;否則,ok
為 false
。
若 key
不在map中,那麼 elem
是該map元素型別零。
同樣的,當從map中讀取某個不存在的鍵時,結果是map射的元素型別的零值。
注 :若 elem
或 ok
還未宣告,你可以使用短變數宣告:
elem, ok := m[key]
10.2 map併發不安全
下面程式碼可能會出現
-
併發讀寫錯誤 fatal error: concurrent map read and map write
-
併發寫錯誤 fatal error: concurrent map writes
package main
import "time"
func main() {
m := make(map[int]int)
for i := 0; i < 100; i++ {
go writeMap(m, i, i)
go readMap(m, i)
}
time.Sleep(time.Second)
}
func readMap(m map[int]int, key int) int {
return m[key]
}
func writeMap(m map[int]int, key int, value int) {
m[key] = value
}
10.3 map加鎖版本
package main
import (
"fmt"
"math/rand"
"strconv"
"sync"
)
type userMap struct {
sync.RWMutex
m map[string]string
}
func (u *userMap) read(key string) {
u.RLock()
defer u.RUnlock()
n := u.m[key]
fmt.Println("value:", n)
}
func (u *userMap) write(key, value string) {
u.Lock()
defer u.Unlock()
u.m[key] = value
}
func main() {
c := userMap{
m: make(map[string]string),
}
for i := 0; i < 1000; i++ {
v := rand.Int31n(9999)
go c.write(strconv.Itoa(i), strconv.Itoa(int(v)))
go c.read(strconv.Itoa(i))
}
}
11 函式
11.1 函式定義
- 定義:函式名、形參列表、返回值列表、函式體
func function_name( [parameter list] ) [return_types] {
函式體
}
func funcName(param1 int,param2 string)(int, error){
return 0,nil
}
-
多返回值
-
函式大寫開頭表示public,包外面可見。小寫開頭表示private,包外部可見
-
引數傳遞是值傳遞
基礎型別、陣列、結構體,在函式內修改,不會影響到原來的值。
slice、map、channel本身是引用型別,在函式內修改,會影響原來的值
-
函式也可以當做變數
-
可變引數,用 … 表示
func myfunc(args ...int) {
for _, arg := range args {
fmt.Println(arg)
}
}
11.1 retrun
- Go 語言支援多個返回值
- 函式返回是分兩步的,1、先給返回值賦值,2、然後再返回
package main
func foo() (int, int) {
i := 1
j := 2
return i, j
}
func main() {
foo()
}
檢視彙編程式碼go tool compile -S -N -l types.go
0x0000 00000 (types.go:3) TEXT "".foo(SB), NOSPLIT|ABIInternal, $24-16
0x0000 00000 (types.go:3) SUBQ $24, SP
0x0004 00004 (types.go:3) MOVQ BP, 16(SP)
0x0009 00009 (types.go:3) LEAQ 16(SP), BP
0x000e 00014 (types.go:3) PCDATA $0, $-2
0x000e 00014 (types.go:3) PCDATA $1, $-2
0x000e 00014 (types.go:3) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x000e 00014 (types.go:3) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x000e 00014 (types.go:3) FUNCDATA $2, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x000e 00014 (types.go:3) PCDATA $0, $0
0x000e 00014 (types.go:3) PCDATA $1, $0
0x000e 00014 (types.go:3) MOVQ $0, "".~r0+32(SP)
0x0017 00023 (types.go:3) MOVQ $0, "".~r1+40(SP)
0x0020 00032 (types.go:4) MOVQ $1, "".i+8(SP)
0x0029 00041 (types.go:5) MOVQ $2, "".j(SP)
0x0031 00049 (types.go:6) MOVQ "".i+8(SP), AX
0x0036 00054 (types.go:6) MOVQ AX, "".~r0+32(SP)
0x003b 00059 (types.go:6) MOVQ "".j(SP), AX
0x003f 00063 (types.go:6) MOVQ AX, "".~r1+40(SP)
0x0044 00068 (types.go:6) MOVQ 16(SP), BP
0x0049 00073 (types.go:6) ADDQ $24, SP
0x004d 00077 (types.go:6) RET
11.2 defer
- 在函式退出時執行
- 多個defer按先進後出方式執行
- defer呼叫在返回值賦值後,在函式返回前
返回值賦值
defer f()
return
package main
import "fmt"
//1、返回值為定義一個臨時變數,賦值為1
//2、defer中i++
//3、return;所以返回值是1,defer中i改變不影響返回值
func foo() int {
i := 1
defer func() {
i++
fmt.Printf("i=%d\n", i)
}()
return i
}
func main() {
res := foo()
fmt.Printf("返回值=%d\n", res)
}
返回值如果是命名返回值,情況又不一樣
package main
import "fmt"
//1、i=1
//2、defer中i=11
//3、return
func foo() (i int) {
defer func() {
i = i + 10
}()
return 1
}
func main() {
res := foo()
fmt.Printf("res=%d\n", res)
}
11.3 閉包函式
閉包是可以包含自由(未繫結到特定物件)變數的程式碼塊,這些變數不在這個程式碼塊內或者
任何全域性上下文中定義,而是在定義程式碼塊的環境中定義。要執行的程式碼塊(由於自由變數包含
在程式碼塊中,所以這些自由變數以及它們引用的物件沒有被釋放)為自由變數提供繫結的計算環
境(作用域)。
應用《Go語言程式設計》的描述,我們可以理解閉包為帶有狀態的函式,由變數和函式兩部分組成。
package main
import (
"fmt"
"strconv"
)
func add() func(int) int {
n := 10
str := "good luck"
return func(x int) int {
n = x + n
str = str+strconv.Itoa(x)
fmt.Println(str)
return n
}
}
func main() {
fn := add()
fmt.Printf("%d\n", fn(1))
fmt.Printf("%d\n", fn(1))
fmt.Printf("%d\n", fn(1))
}
閉包可以看做下面變數和匿名函式的結合
n := 10
str := "good luck"
return func(x int) int {
n = x + n
str += strconv.Itoa(x)
fmt.Println(str)
return n
}
12 介面interface
12.1 Any型別
由於Go語言中任何物件例項都滿足空介面interface{},所以interface{}看起來像是可
以指向任何物件的Any型別,如下:
var v1 interface{} = 1 // 將int型別賦值給interface{}
var v2 interface{} = "abc" // 將string型別賦值給interface{}
var v3 interface{} = &v2 // 將*interface{}型別賦值給interface{}
var v4 interface{} = struct{ X int }{1}
var v5 interface{} = &struct{ X int }{1}
當函式可以接受任意的物件例項時,我們會將其宣告為interface{},最典型的例子是標
準庫fmt中PrintXXX系列的函式,例如:
func Printf(fmt string, args ...interface{})
func Println(args ...interface{})
...
12.2 型別斷言
型別斷言 提供了訪問介面值底層具體值的方式。
t := i.(T)
該語句斷言介面值 i
儲存了具體型別 T
,並將其底層型別為 T
的值賦予變數 t
。
若 i
並未儲存 T
型別的值,該語句就會觸發一個恐慌。
為了 判斷 一個介面值是否儲存了一個特定的型別,型別斷言可返回兩個值:其底層值以及一個報告斷言是否成功的布林值。
t, ok := i.(T)
若 i
儲存了一個 T
,那麼 t
將會是其底層值,而 ok
為 true
。
否則,ok
將為 false
而 t
將為 T
型別的零值,程式並不會產生恐慌。
請注意這種語法和讀取一個對映時的相同之處
package main
import "fmt"
func main() {
var i interface{} = "hello"
s := i.(string)
fmt.Println(s)
s, ok := i.(string)
fmt.Println(s, ok)
f, ok := i.(float64)
fmt.Println(f, ok)
f = i.(float64) // 報錯(panic)
fmt.Println(f)
}
12.3 型別選擇
型別選擇 是一種按順序從幾個型別斷言中選擇分支的結構。
型別選擇與一般的 switch 語句相似,不過型別選擇中的 case 為型別(而非值), 它們針對給定介面值所儲存的值的型別進行比較。
switch v := i.(type) {
case T:
// v 的型別為 T
case S:
// v 的型別為 S
default:
// 沒有匹配,v 與 i 的型別相同
}
型別選擇中的宣告與型別斷言 i.(T)
的語法相同,只是具體型別 T
被替換成了關鍵字 type
。
此選擇語句判斷介面值 i
儲存的值型別是 T
還是 S
。在 T
或 S
的情況下,變數 v
會分別按 T
或 S
型別儲存 i
擁有的值。在預設(即沒有匹配)的情況下,變數 v
與 i
的介面型別和值相同。
package main
import "fmt"
func do(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("Twice %v is %v\n", v, v*2)
case string:
fmt.Printf("%q is %v bytes long\n", v, len(v))
default:
fmt.Printf("I don't know about type %T!\n", v)
}
}
func main() {
do(21)
do("hello")
do(true)
}
12.4 非侵入式介面
在Go語言中,一個類只需要實現了介面要求的所有函式,我們就說這個類實現了該介面,
例如:
type File struct {
// ...
}
func (f *File) Read(buf []byte) (n int, err error){
...
}
func (f *File) Write(buf []byte) (n int, err error){
...
}
func (f *File) Seek(off int64, whence int) (pos int64, err error){
...
}
func (f *File) Close() error{
...
}
這裡我們定義了一個File類,並實現有Read()、Write()、Seek()、Close()等方法。設
想我們有如下介面:
type IFile interface {
Read(buf []byte) (n int, err error)
Write(buf []byte) (n int, err error)
Seek(off int64, whence int) (pos int64, err error)
Close() error
}
type IReader interface {
Read(buf []byte) (n int, err error)
}
type IWriter interface {
Write(buf []byte) (n int, err error)
}
type ICloser interface {
Close() error
}
儘管File類並沒有從這些介面繼承,甚至可以不知道這些介面的存在,但是File類實現了
這些介面,可以進行賦值:
var file1 IFile = new(File)
var file2 IReader = new(File)
var file3 IWriter = new(File)
var file4 ICloser = new(File)
13 協程goroutine
-
goroutine 是輕量級執行緒,goroutine 的排程是由 Golang 執行時進行管理的。
-
執行緒和goroutine區別
在linux作業系統中,執行緒是一種輕量級程序,由作業系統來排程。執行緒切換需要由使用者態切換到核心態進行,將一些執行緒資源儲存在核心記憶體空間(32位系統是3-4G),然後從核心記憶體空間恢復被排程的下一個執行緒資源。
goroutine是真正意義上的執行緒,go實現了goroutine的管理,goroutine的切換隻在使用者態進行。
-
Don’t communicate by sharing memory; share memory by communicating.不要通過共享資料來通訊,恰恰相反,要以通訊的方式共享資料。
-
在業務量併發大的情況下,請注意控制goroutine數量,你後端的程式效能跟不上(資料庫),你程式併發支援再多也沒用。請使用協程池。
goroutine 語法格式:
go 函式名(引數列表)
14 channel
-
channel是併發安全的
-
型別字面量如
chan int
,其中的chan
是表示通道型別的關鍵字,而int
則說明了該通道型別的元素型別 -
可以使用make方法來建立,第二個引數代表通道緩衝容量,0或者沒有代表非緩衝通道
-
是引用型別,零值是nil,寫或讀零值channel,會一直阻塞
-
通道是先進先出。阻塞時,傳送有傳送佇列,接收有接收佇列,通道可傳送或可接收時,通知佇列裡面最新等待發送或接收的goroutine
-
資料進入通道是複製右邊的元素到通道(注意引用型別,只複製元素本身,他指向的資料還是不變)
-
channel在close操作後,繼續傳送資料會導致panic。close的通道可以繼續讀取資料,可以定義兩個變數接收讀取通道的結果
-
一般在傳送端關閉通道
//通道關閉的時候,讀取完通道資料後,ok=false,所以在讀取的時候注意判斷ok的值 value, ok := <-c
-
阻塞情況
有緩衝區的通道:1、寫入緩衝已滿的通道會阻塞。2、讀取沒有資料的通道。
無緩衝通道:傳送或接收有一方沒準備,就會阻塞在那裡
14.1 宣告
//未賦值的channel的值是nil
var c chan int
//宣告並賦值
var c = make(chan int, 10)
c := make(chan int, 10)
- 寫入和讀取chan,用<-表示
package main
import (
"fmt"
"time"
)
func main() {
//var c chan int = make(chan int, 10)
c := make(chan int, 10)
//寫入
c <- 1
c <- 2
c <- 3
go func() {
for {
//讀取
value, ok := <-c
if ok {
fmt.Println(value)
} else {
fmt.Println("channel close")
time.Sleep(2 * time.Second)
}
}
}()
c <- 4
close(c)
time.Sleep(2 * time.Second)
}
14.2 channel的for range
- 這樣一條
for
語句會不斷地嘗試從chan種取出元素值,即使chan被關閉,它也會在取出所有剩餘的元素值之後再結束執行。
package main
import (
"fmt"
)
func main() {
num := 5
ch := make(chan int, num)
for i := 0; i < num; i++ {
ch <- i
}
close(ch)
for elem := range ch {
fmt.Printf("The element is: %v\n", elem)
}
fmt.Println("666")
}
- 當chan中沒有元素值時,它會被阻塞在有
for
關鍵字的那一行,直到有新的元素值可取。
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int, 5)
go func() {
for elem := range ch {
fmt.Printf("The element is: %v\n", elem)
}
}()
time.Sleep(5 * time.Second)
fmt.Println("start send element")
for i := 0; i < 5; i++ {
ch <- i
}
close(ch)
fmt.Println("close channel")
time.Sleep(1 * time.Second)
}
- 假設
chan
的沒有賦值,那麼它會被永遠阻塞在有for
關鍵字的那一行。
/*
這段程式碼會報fatal error: all goroutines are asleep - deadlock!
所有協程都阻塞,for後面的程式碼執行不到了
*/
package main
import (
"fmt"
)
func main() {
var ch chan int
for elem := range ch {
fmt.Printf("The elem is: %v\n", elem)
}
fmt.Println("666")
}
14.3 select
select
語句是專為通道而設計的,每個case
表示式中都只能包含操作通道的表示式,接收表示式和傳送表示式。- 進入select語句時,所有case的chan都阻塞情況下,會執行default語句
select
語句發現同時有多個候選case分支滿足選擇條件,那麼它就會用一種偽隨機的演算法在這些分支中選擇一個並執行- 僅當
select
語句中的所有case
表示式都被求值完畢後,它才會開始選擇候選分支。 - select中也是有break語法,在for迴圈中的select要注意,break是跳出最近的select、switch、for。請注意break的坑
//多執行幾次該程式,可以看到select的case分支是隨機的
//多執行幾次該程式,可以看到select的case分支是隨機的
package main
import (
"fmt"
"math/rand"
"time"
)
func send(ch chan int) {
value := rand.Int31n(9999)
ch <- int(value)
}
func main() {
rand.Seed(time.Now().UnixNano())
ch1 := make(chan int, 1)
ch2 := make(chan int, 1)
send(ch1)
send(ch2)
time.Sleep(1000)
timer := time.NewTimer(3 * time.Second).C
loop:
for {
select {
case v := <-ch1:
fmt.Println("ch1通道獲取到資料", v)
case v := <-ch2:
fmt.Println("ch2通道獲取到資料", v)
case <-timer:
fmt.Println("退出for迴圈")
break loop
//default:
// time.Sleep(1000)
// fmt.Println("通道中沒有資料")
}
}
}
注意:case表示式都是先求值的,可以實驗下面例子
package main
import (
"fmt"
"math/rand"
"time"
)
func send(ch chan int) {
value := rand.Int31n(9999)
ch <- int(value)
}
func recv(ch chan int, id int) <-chan int {
fmt.Println(id, "在這裡可以進行一些邏輯")
return ch
}
func main() {
rand.Seed(time.Now().UnixNano())
ch1 := make(chan int, 1)
ch2 := make(chan int, 1)
send(ch1)
send(ch2)
time.Sleep(1000)
timer := time.NewTimer(3 * time.Second).C
loop:
for {
select {
case v := <-recv(ch1, 1):
fmt.Println("ch1通道獲取到資料", v)
case v := <-recv(ch2, 2):
fmt.Println("ch2通道獲取到資料", v)
case <-timer:
fmt.Println("退出for迴圈")
break loop
//default:
// fmt.Println("通道中沒有資料")
}
}
}
/*結果
1 在這裡可以進行一些邏輯
2 在這裡可以進行一些邏輯
ch1通道獲取到資料 795
1 在這裡可以進行一些邏輯
2 在這裡可以進行一些邏輯
ch2通道獲取到資料 5534
1 在這裡可以進行一些邏輯
2 在這裡可以進行一些邏輯
退出for迴圈
*/
14.4 單方向channel的應用
var send chan<- int// 只能傳送
var recv <-chan int // 只能接收
// 一般用於介面中約束某個方法中的channel只能用於傳送資料或者接收資料,在傳入引數的時候是可以傳雙向的channel
type Notifier interface {
SendInt(ch chan<- int)
RcevInt(ch <-chan int)
}