網(wǎng)頁制作題庫哈爾濱網(wǎng)站優(yōu)化流程
Go基礎(chǔ)之變量和常量
文章目錄
- Go基礎(chǔ)之變量和常量
- 一. 標(biāo)識(shí)符、關(guān)鍵字、內(nèi)置類型和函數(shù)
- 1.1 標(biāo)識(shí)符
- 1.2 關(guān)鍵字
- 1.3 保留字
- 1.4 內(nèi)置類型
- 1.4.1 值類型:
- 1.4.2 引用類型:(指針類型)
- 1.5 內(nèi)置函數(shù)
- 1.6 內(nèi)置接口error
- 二.Go變量命名規(guī)范
- 2.1 采用駝峰體命名
- 2.2 簡單、短小為首要原則
- 2.3 變量名字中不要帶有類型信息
- 2.4 保持簡短命名變量含義上的一致性
- 2.5 常量命名規(guī)則
- 三、變量
- 3.1 變量的來歷
- 3.2 聲明變量
- 3.3 標(biāo)準(zhǔn)聲明(聲明單個(gè)變量)
- 3.4 默認(rèn)初始值
- 3.5 聲明多個(gè)變量
- 3.6 變量聲明的“語法糖”
- 3.6.1 短變量聲明
- 3.6.2 省略類型信息的聲明
- 3.7 匿名變量
- 四、包級(jí)變量聲明
- 4.1 包級(jí)變量介紹
- 4.2 包級(jí)變量的聲明形式分類
- 4.2.1 第一類:聲明并同時(shí)顯式初始化
- 4.2.2 第二類:聲明但延遲初始化
- 五、局部變量聲明
- 5.1 局部變量介紹
- 5.2 局部變量聲明形式分類
- 5.2.1 第一類:對(duì)于延遲初始化的局部變量聲明,我們采用通用的變量聲明形式
- 5.2.2 第二類:對(duì)于聲明且顯式初始化的局部變量,建議使用短變量聲明形式
- 六、變量聲明小結(jié)
- 七、常量
- 7.1 常量定義
- 7.2 聲明多個(gè)常量
- 7.3 常量的創(chuàng)新點(diǎn)
- 7.3.1 無類型常量
- 7.3.2 隱式轉(zhuǎn)型
- 7.3.3 實(shí)現(xiàn)枚舉
- iota 介紹
- 幾個(gè)常見的`iota`示例:
一. 標(biāo)識(shí)符、關(guān)鍵字、內(nèi)置類型和函數(shù)
1.1 標(biāo)識(shí)符
在編程語言中標(biāo)識(shí)符就是程序員定義的具有特殊意義的詞,比如變量名、常量名、函數(shù)名等等。 Go語言中標(biāo)識(shí)符由字母數(shù)字和_
(下劃線)組成,并且只能以字母和_
開頭。 舉幾個(gè)例子:abc
, _
, _123
, a123
。
1.2 關(guān)鍵字
Go語言中關(guān)鍵字有25個(gè);關(guān)鍵字不能用于自定義名字,只能再特定語法結(jié)構(gòu)中使用。
Go語言中有25個(gè)關(guān)鍵字:
break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var
1.3 保留字
此外,Go語言中還有37個(gè)保留字
- 保留字:主要對(duì)應(yīng)內(nèi)建的常量、類型和函數(shù)
Constants: true false iota nil內(nèi)建函數(shù)Types: int int8 int16 int32 int64 內(nèi)建類型 uint uint8 uint16 uint32 uint64 uintptrfloat32 float64 complex128 complex64bool byte rune string errorFunctions: make len cap new append copy close delete內(nèi)建函數(shù) complex real imagpanic recover
1.4 內(nèi)置類型
1.4.1 值類型:
boolint(32 or 64), int8, int16, int32, int64uint(32 or 64), uint8(byte), uint16, uint32, uint64float32, float64stringcomplex64, complex128array -- 固定長度的數(shù)組
1.4.2 引用類型:(指針類型)
slice -- 序列數(shù)組(最常用)map -- 映射chan -- 管道
1.5 內(nèi)置函數(shù)
Go 語言擁有一些不需要進(jìn)行導(dǎo)入操作就可以使用的內(nèi)置函數(shù)。它們有時(shí)可以針對(duì)不同的類型進(jìn)行操作,例如:len、cap 和 append,或必須用于系統(tǒng)級(jí)的操作,例如:panic。因此,它們需要直接獲得編譯器的支持。
append -- 用來追加元素到數(shù)組、slice中,返回修改后的數(shù)組、sliceclose -- 主要用來關(guān)閉channeldelete -- 從map中刪除key對(duì)應(yīng)的valuepanic -- 停止常規(guī)的goroutine (panic和recover:用來做錯(cuò)誤處理)recover -- 允許程序定義goroutine的panic動(dòng)作real -- 返回complex的實(shí)部 (complex、real imag:用于創(chuàng)建和操作復(fù)數(shù))imag -- 返回complex的虛部make -- 用來分配內(nèi)存,返回Type本身(只能應(yīng)用于slice, map, channel)new -- 用來分配內(nèi)存,主要用來分配值類型,比如int、struct。返回指向Type的指針cap -- capacity是容量的意思,用于返回某個(gè)類型的最大容量(只能用于切片和 map)copy -- 用于復(fù)制和連接slice,返回復(fù)制的數(shù)目len -- 來求長度,比如string、array、slice、map、channel ,返回長度print、println -- 底層打印函數(shù),在部署環(huán)境中建議使用 fmt 包
1.6 內(nèi)置接口error
type error interface { //只要實(shí)現(xiàn)了Error()函數(shù),返回值為String的都實(shí)現(xiàn)了err接口Error() String}
二.Go變量命名規(guī)范
Go語言中的函數(shù)、變量、常量、類型、方法所有的命名,都遵循一個(gè)簡單的命名規(guī)則:
2.1 采用駝峰體命名
采用CamelCase
駝峰命名法**(官方推薦)**
- 如果只在包內(nèi)可用,就采用小駝峰命名,即:
lowerCamelCase
- 如果要在包外可見,就采用大駝峰命名,即:
UpperCamelCase
2.2 簡單、短小為首要原則
為變量、類型、函數(shù)和方法命名時(shí)依然要以簡單、短小為首要原則。我們對(duì)Go標(biāo)準(zhǔn)庫(Go 1.12版本)中標(biāo)識(shí)符名稱進(jìn)行統(tǒng)計(jì)的結(jié)果如下(去除Go關(guān)鍵字和builtin函數(shù)):
// 在$GOROOT/src下$cat $(find . -name '*.go') | indents | sort | uniq -c | sort -nr | sed 30q
105896 v
71894 err
54512 Args
49472 t
44090 _
43881 x
43322 b
36019 i
34432 p
32011 s
28435 AddArg
26185 c
25518 n
25242 e1
23881 r
21681 AuxInt
20700 y
...”
我們看到了大量單字母的標(biāo)識(shí)符命名,這是Go在命名上的一個(gè)慣例。一般來說,Go標(biāo)識(shí)符仍以單個(gè)單詞作為命名首選。從Go標(biāo)準(zhǔn)庫代碼的不完全統(tǒng)計(jì)結(jié)果來看,不同類別標(biāo)識(shí)符的命名呈現(xiàn)出以下特征:
- 函數(shù)、變量、常量、類型、方法命名遵循簡單、短小為首要原則
- 函數(shù)/方法的參數(shù)和返回值變量以單個(gè)單詞或單個(gè)字母為主;
- 由于方法在調(diào)用時(shí)會(huì)綁定類型信息,因此方法的命名以單個(gè)單詞為主;
- 函數(shù)多以多單詞的復(fù)合詞進(jìn)行命名;類型多以多單詞的復(fù)合詞進(jìn)行命名。
- 條件、循環(huán)變量可以是單個(gè)字母或單個(gè)單詞,Go傾向于使用單個(gè)字母。Go建議使用更短小
- 包以小寫單個(gè)單詞命名,包名應(yīng)該和導(dǎo)入路徑的最后一段路徑保持一致
- 接口優(yōu)先采用單個(gè)單詞命名,一般加er后綴。Go語言推薦盡量定義小接口,接口也可以組合
- 命名要簡短有意義,關(guān)鍵字和保留字都不建議用作變量名
package mainimport "fmt"func main() {cityName := "北京" // 駝峰式命名(官方推薦)city_name := "上海" //下劃線式fmt.Println(studentNameIsASheng, student_name_is_A_lian)}
2.3 變量名字中不要帶有類型信息
比如以下命名:
userSlice []*User users []*User[bad] [good]
帶有類型信息的命名只是讓變量看起來更長,并沒有給開發(fā)者閱 讀代碼帶來任何好處。
不過有些開發(fā)者會(huì)認(rèn)為:userSlice中的類型信息可以告訴我們變 量所代表的底層存儲(chǔ)是一個(gè)切片,這樣便可以在userSlice上應(yīng)用切片 的各種操作了。提出這樣質(zhì)疑的開發(fā)者顯然忘記了一條編程語言命名的慣例:保持變量聲明與使用之間的距離越近越好,或者在第一次使 用變量之前聲明該變量。這個(gè)慣例與Go核心團(tuán)隊(duì)的Andrew Gerrard曾 說的“一個(gè)名字的聲明和使用之間的距離越大,這個(gè)名字的長度就越 長”異曲同工。如果在一屏之內(nèi)能看到users的聲明,那么-Slice這個(gè)類 型信息顯然不必放在變量的名稱中了。
2.4 保持簡短命名變量含義上的一致性
從上面的統(tǒng)計(jì)可以看到,Go語言中有大量單字母、單個(gè)詞或縮寫 命名的簡短命名變量。有人可能會(huì)認(rèn)為簡短命名變量會(huì)降低代碼的可讀性。Go語言建議通過保持一致性來維持可讀性。一致意味著代碼中相同或相似的命名所傳達(dá)的含義是相同或相似的,這樣便于代碼閱讀者或維護(hù)者猜測出變量的用途。
這里大致分析一下Go標(biāo)準(zhǔn)庫中常見短變量名字所代表的含義,這 些含義在整個(gè)標(biāo)準(zhǔn)庫范疇內(nèi)的一致性保持得很好。
變量v、k、i
的常用含義:
// 循環(huán)語句中的變量
for i, v := range s {// i 通常用作下標(biāo)變量; v 通常用作元素值// 在這里執(zhí)行你的循環(huán)邏輯
}for k, v := range m {// k 通常用作鍵(key)變量; v 通常用作元素值// 在這里執(zhí)行你的循環(huán)邏輯
}for v := range r {// v 通常用作元素值// 在這里執(zhí)行你的循環(huán)邏輯,這通常用于接收 channel 中的值
}// 在 if 語句中使用變量 v
if v := mimeTypes[ext]; v != "" {// v 通常用作元素值// 在這里執(zhí)行你的條件邏輯
}// 在 switch/case 語句中使用變量 v
switch v := ptr.Elem(); v.Kind() {
case reflect.Int:// v 通常用作元素值// 在這里執(zhí)行你的條件邏輯
}// 在 select 語句的 case 中使用變量 v
select {
case v := <-c:// v 通常用作元素值// 在這里執(zhí)行你的條件邏輯
}// 創(chuàng)建反射值 v
v := reflect.ValueOf(x)
變量 t
的常用含義
t := time.Now() // 通常用于表示時(shí)間
t := &time.Timer{} // 通常用于表示定時(shí)器
if t := md.typemap[off]; t != nil {// 通常用于表示類型
}
變量 b
的常用含義
b := make([]byte, n) // 通常用于表示 byte 切片
b := new(bytes.Buffer) // 通常用于表示 byte 緩沖
2.5 常量命名規(guī)則
以下是一些常見的Go常量命名約定:
- 常量名:通常使用駝峰命名法(CamelCase)來命名常量。
- 全大寫:雖然Go中不要求常量名全部大寫,但在實(shí)際編碼中,全大寫的常量名仍然是一種常見的約定,特別是對(duì)于導(dǎo)出的常量(首字母大寫的常量,可在包外部訪問)。
- 類型信息:Go中的數(shù)值型常量通常不需要顯式賦予類型信息,因?yàn)镚o會(huì)根據(jù)上下文進(jìn)行類型推斷。因此,常量的名字通常不包含類型信息。
- 多單詞組合:常量名通常會(huì)使用多個(gè)單詞組合以傳達(dá)更準(zhǔn)確的含義,這有助于提高代碼的可讀性。例如,
defaultMaxMemory
和deleteHostHeader
。 - 不同包內(nèi)的常量:對(duì)于導(dǎo)出的常量,可以考慮使用全大寫的常量名以確保在其他包中能夠訪問。
下面是標(biāo)準(zhǔn)庫中的例子經(jīng)過格式化的代碼:
// $GOROOT/src/net/http/request.goconst (defaultMaxMemory = 32 << 20 // 32 MB
)const (deleteHostHeader = truekeepHostHeader = false
)// $GOROOT/src/math/sin.go
const (PI4A = 7.85398125648498535156E-1 // 0x3fe921fb40000000,PI4B = 3.77489470793079817668E-8 // 0x3e64442d00000000,PI4C = 2.69515142907905952645E-15 // 0x3ce8469898cc5170,)// $GOROOT/src/syscall/zerrors_linux_amd64.go
// 錯(cuò)誤碼
const (E2BIG = Errno(0x7)EACCES = Errno(0xd)EADDRINUSE = Errno(0x62)EADDRNOTAVAIL = Errno(0x63)EADV = Errno(0x44)
)// 信號(hào)
const (SIGABRT = Signal(0x6)SIGALRM = Signal(0xe)SIGBUS = Signal(0x7)SIGCHLD = Signal(0x11)
)
三、變量
3.1 變量的來歷
在編程語言中,為了方便操作內(nèi)存特定位置的數(shù)據(jù),我們用一個(gè)特定的名字與位于特定位置的內(nèi)存塊綁定在一起,這個(gè)名字被稱為變量。
但這并不代表我們可以通過變量隨意引用或修改內(nèi)存,變量所綁定的內(nèi)存區(qū)域是要有一個(gè)明確的邊界的。也就是說,通過這樣一個(gè)變量,我們究竟可以操作 4 個(gè)字節(jié)內(nèi)存還是 8 個(gè)字節(jié)內(nèi)存,又或是 256 個(gè)字節(jié)內(nèi)存,編程語言的編譯器或解釋器需要明確地知道。綜上:變量就是指定了某存儲(chǔ)單元(Memory Location)的名稱,該存儲(chǔ)單元會(huì)存儲(chǔ)特定類型的值。
那么,編程語言的編譯器或解釋器是如何知道一個(gè)變量所能引用的內(nèi)存區(qū)域邊界呢?
其實(shí),動(dòng)態(tài)語言和靜態(tài)語言有不同的處理方式。動(dòng)態(tài)語言(比如 Python、Ruby 等)的解釋器可以在運(yùn)行時(shí)通過對(duì)變量賦值的分析,自動(dòng)確定變量的邊界。并且在動(dòng)態(tài)語言中,一個(gè)變量可以在運(yùn)行時(shí)被賦予大小不同的邊界。
而靜態(tài)編程語言在這方面的“體驗(yàn)略差”。靜態(tài)類型語言編譯器必須明確知道一個(gè)變量的邊界才允許使用這個(gè)變量,但靜態(tài)語言編譯器又沒能力自動(dòng)提供這個(gè)信息,這個(gè)邊界信息必須由這門語言的使用者提供,于是就有了“變量聲明”。通過變量聲明,語言使用者可以顯式告知編譯器一個(gè)變量的邊界信息。在具體實(shí)現(xiàn)層面呢,這個(gè)邊界信息由變量的類型屬性賦予。
作為身處靜態(tài)編程語言陣營的 Go 語言,它沿襲了靜態(tài)語言的這一要求:使用變量之前需要先進(jìn)行變量聲明。
3.2 聲明變量
Go語言中的變量必須聲明后才能使用,同一作用域內(nèi)不支持重復(fù)聲明。 并且Go語言的變量聲明后必須使用。
3.3 標(biāo)準(zhǔn)聲明(聲明單個(gè)變量)
在 Go 語言中,有一個(gè)通用的變量聲明方法是這樣的:
這個(gè)變量聲明分為四個(gè)部分:
- var 是修飾變量聲明的關(guān)鍵字;
- a 為變量名;
- int 為該變量的類型;
- 10 是變量的初值。
3.4 默認(rèn)初始值
如果你沒有顯式為變量賦予初值,Go 編譯器會(huì)為變量賦予這個(gè)類型的零值:
var a int // a的初值為int類型的零值:0
什么是類型的零值呢?Go 語言的每種原生類型都有它的默認(rèn)值,這個(gè)默認(rèn)值就是這個(gè)類型的零值。以下是內(nèi)置原生類型的默認(rèn)值(即零值):
內(nèi)置原生類型 | 默認(rèn)值(零值) |
---|---|
所有整型類型 | 0 |
浮點(diǎn)類型 | 0.0 |
布爾類型 | false |
字符串類型 | “” |
指針、接口、切片、channel、 map和函數(shù)類型 | nil |
3.5 聲明多個(gè)變量
每聲明一個(gè)變量就需要寫var
關(guān)鍵字會(huì)比較繁瑣,go語言中還支持批量變量聲明:
var (a int = 128b int8 = 6s string = "hello"c rune = 'A't bool = true
)
Go 語言還支持在一行變量聲明中同時(shí)聲明多個(gè)變量:
var a, b, c int = 5, 6, 7
這樣的多變量聲明同樣也可以用在變量聲明塊中,像下面這樣:
var (a, b, c int = 5, 6, 7c, d, e rune = 'C', 'D', 'E'
)
簡單示例:
package mainimport "fmt"func main() {var (name string = "jarvis"age int = 18height int = 168)fmt.Println("my name is", name, ", age is", age, "and height is", height)
}
3.6 變量聲明的“語法糖”
3.6.1 短變量聲明
在函數(shù)內(nèi)部,可以使用更簡略的 :=
方式聲明并初始化變量。標(biāo)準(zhǔn)范式如下:
varName := initExpression
簡單示例:
package mainimport "fmt"func main() { name, age := "naveen", 29 // 簡短聲明fmt.Println("my name is", name, "age is", age)
}
運(yùn)行上面的程序,可以看到輸出為 my name is naveen age is 29
。
簡短聲明要求 := 操作符左邊的所有變量都有初始值。下面程序?qū)?huì)拋出錯(cuò)誤 cannot assign 1 values to 2 variables
,這是因?yàn)?age 沒有被賦值。
package mainimport "fmt"func main() { name, age := "naveen" //errorfmt.Println("my name is", name, "age is", age)
}
3.6.2 省略類型信息的聲明
Go 編譯器允許我們省略變量聲明中的類型信息,標(biāo)準(zhǔn)范式如下:
var varName = initExpression
比如下面就是一個(gè)省略了類型信息的變量聲明:
var b = 13
那么 Go 編譯器在遇到這樣的變量聲明后是如何確定變量的類型信息呢?
其實(shí)很簡單,Go 編譯器會(huì)根據(jù)右側(cè)變量初值自動(dòng)推導(dǎo)出變量的類型,并給這個(gè)變量賦予初值所對(duì)應(yīng)的默認(rèn)類型。比如,整型值的默認(rèn)類型 int,浮點(diǎn)值的默認(rèn)類型為 float64,復(fù)數(shù)值的默認(rèn)類型為 complex128。其他類型值的默認(rèn)類型就更好分辨了,在 Go 語言中僅有唯一與之對(duì)應(yīng)的類型,比如布爾值的默認(rèn)類型只能是 bool,字符值默認(rèn)類型只能是 rune,字符串值的默認(rèn)類型只能是 string 等。
如果我們不接受默認(rèn)類型,而是要顯式地為變量指定類型,除了通用的聲明形式,我們還可以通過顯式類型轉(zhuǎn)型達(dá)到我們的目的:
var b = int32(13)
顯然這種省略類型信息聲明的“語法糖”僅適用于在變量聲明的同時(shí)顯式賦予變量初值的情況,下面這種沒有初值的聲明形式是不被允許的:
var b
結(jié)合多變量聲明,我們可以使用這種變量聲明“語法糖”聲明多個(gè)不同類型的變量:
package mainimport "fmt"func main() {var a, b, c = 12, 'A', "hello"fmt.Printf("a type: %T\n", a)fmt.Printf("b type: %T\n", b)fmt.Printf("c type: %T\n", c)
}// 輸出
a type: int
b type: int32
c type: string
在這個(gè)變量聲明中,我們聲明了三個(gè)變量 a、b 和 c,但它們分別具有不同的類型,分別為 int、rune 和 string。在這種變量聲明語法糖中,我們省去了變量類型信息,但 Go 編譯器會(huì)為我們自動(dòng)推導(dǎo)出類型信息。
3.7 匿名變量
在使用多重賦值時(shí),如果想要忽略某個(gè)值,可以使用匿名變量(anonymous variable)
。 匿名變量用一個(gè)下劃線_
表示,例如:
func foo() (int, string) {return 10, "babyboy"
}
func main() {x, _ := foo()_, y := foo()fmt.Println("x=", x)fmt.Println("y=", y)
}
匿名變量不占用命名空間,不會(huì)分配內(nèi)存,所以匿名變量之間不存在重復(fù)聲明。 (在Lua
等編程語言里,匿名變量也被叫做啞元變量。)
注意事項(xiàng):
- 函數(shù)外的每個(gè)語句都必須以關(guān)鍵字開始(var、const、func等)
:=
不能使用在函數(shù)外。_
多用于占位,表示忽略值。
四、包級(jí)變量聲明
4.1 包級(jí)變量介紹
- 包級(jí)變量 (package varible),也就是在包級(jí)別可見的變量。如果是導(dǎo)出變量(大寫字母開頭),那么這個(gè)包級(jí)變量也可以被視為全局變量
- 包級(jí)變量只能使用帶有 var 關(guān)鍵字的變量聲明形式,不能使用短變量聲明形式,但在形式細(xì)節(jié)上可以有一定靈活度。
4.2 包級(jí)變量的聲明形式分類
4.2.1 第一類:聲明并同時(shí)顯式初始化
先看看這個(gè)代碼:
// $GOROOT/src/io/io.go
var ErrShortWrite = errors.New("short write")
var ErrShortBuffer = errors.New("short buffer")
var EOF = errors.New("EOF")
我們可以看到,這個(gè)代碼塊里聲明的變量都是 io 包的包級(jí)變量。在 Go 標(biāo)準(zhǔn)庫中,對(duì)于變量聲明的同時(shí)進(jìn)行顯式初始化的這類包級(jí)變量,實(shí)踐中多使用這種省略類型信息的“語法糖”格式:
var varName = initExpression
就像我們前面說過的那樣,Go 編譯器會(huì)自動(dòng)根據(jù)等號(hào)右側(cè) InitExpression 結(jié)果值的類型,來確定左側(cè)聲明的變量的類型,這個(gè)類型會(huì)是結(jié)果值對(duì)應(yīng)類型的默認(rèn)類型。
當(dāng)然,如果我們不接受默認(rèn)類型,而是要顯式地為包級(jí)變量指定類型,那么我們有兩種方式,我這里給出了兩種包級(jí)變量的聲明形式的對(duì)比示例。
//第一種:
plain
var a = 13 // 使用默認(rèn)類型
var b int32 = 17 // 顯式指定類型
var f float32 = 3.14 // 顯式指定類型//第二種:
var a = 13 // 使用默認(rèn)類型
var b = int32(17) // 顯式指定類型
var f = float32(3.14) // 顯式指定類型
雖然這兩種方式都是可以使用的,但從聲明一致性的角度出發(fā),Go 更推薦我們使用后者,這樣能統(tǒng)一接受默認(rèn)類型和顯式指定類型這兩種聲明形式,尤其是在將這些變量放在一個(gè) var 塊中聲明時(shí),你會(huì)更明顯地看到這一點(diǎn)。
所以我們更青睞下面這樣的形式:
var (a = 13b = int32(17)f = float32(3.14)
)
4.2.2 第二類:聲明但延遲初始化
對(duì)于聲明時(shí)并不立即顯式初始化的包級(jí)變量,我們可以使用下面這種通用變量聲明形式:
var a int32
var f float64
我們知道,雖然沒有顯式初始化,Go 語言也會(huì)讓這些變量擁有初始的“零值”。如果是自定義的類型,我也建議你盡量保證它的零值是可用的。
這里還有一個(gè)注意事項(xiàng),就是聲明聚類與就近原則。
正好,Go 語言提供了變量聲明塊用來把多個(gè)的變量聲明放在一起,并且在語法上也不會(huì)限制放置在 var 塊中的聲明類型,那我們就應(yīng)該學(xué)會(huì)充分利用 var 變量聲明塊,讓我們變量聲明更規(guī)整,更具可讀性,現(xiàn)在我們就來試試看。
通常,我們會(huì)將同一類的變量聲明放在一個(gè) var 變量聲明塊中,不同類的聲明放在不同的 var 聲明塊中,比如下面就是我從標(biāo)準(zhǔn)庫 net 包中摘取的兩段變量聲明代碼:
// $GOROOT/src/net/net.govar (netGo bool netCgo bool
)var (aLongTimeAgo = time.Unix(1, 0)noDeadline = time.Time{}noCancel = (chan struct{})(nil)
)
我們可以看到,上面這兩個(gè) var 聲明塊各自聲明了一類特定用途的包級(jí)變量。那我就要問了,你還能從中看出什么包級(jí)變量聲明的原則嗎?
其實(shí),我們可以將延遲初始化的變量聲明放在一個(gè) var 聲明塊 (比如上面的第一個(gè) var 聲明塊),然后將聲明且顯式初始化的變量放在另一個(gè) var 塊中(比如上面的第二個(gè) var 聲明塊),這里我稱這種方式為“聲明聚類”,聲明聚類可以提升代碼可讀性。
到這里,你可能還會(huì)有一個(gè)問題:我們是否應(yīng)該將包級(jí)變量的聲明全部集中放在源文件頭部呢?答案不能一概而論。
使用靜態(tài)編程語言的開發(fā)人員都知道,變量聲明最佳實(shí)踐中還有一條:就近原則。也就是說我們盡可能在靠近第一次使用變量的位置聲明這個(gè)變量。就近原則實(shí)際上也是對(duì)變量的作用域最小化的一種實(shí)現(xiàn)手段。在 Go 標(biāo)準(zhǔn)庫中我們也很容易找到符合就近原則的變量聲明的例子,比如下面這段標(biāo)準(zhǔn)庫 http 包中的代碼就是這樣:
// $GOROOT/src/net/http/request.govar ErrNoCookie = errors.New("http: named cookie not present")
func (r *Request) Cookie(name string) (*Cookie, error) {for _, c := range readCookies(r.Header, name) {return c, nil}return nil, ErrNoCookie
}
在這個(gè)代碼塊里,ErrNoCookie 這個(gè)變量在整個(gè)包中僅僅被用在了 Cookie 方法中,因此它被聲明在緊鄰 Cookie 方法定義的地方。當(dāng)然了,如果一個(gè)包級(jí)變量在包內(nèi)部被多處使用,那么這個(gè)變量還是放在源文件頭部聲明比較適合的。
五、局部變量聲明
5.1 局部變量介紹
- 局部變量 (local varible),也就是 Go 函數(shù)或方法體內(nèi)聲明的變量,僅在函數(shù)或方法體內(nèi)可見
- 它們的生命周期僅限于函數(shù)執(zhí)行期間。
5.2 局部變量聲明形式分類
5.2.1 第一類:對(duì)于延遲初始化的局部變量聲明,我們采用通用的變量聲明形式
其實(shí),我們之前講過的省略類型信息的聲明和短變量聲明這兩種“語法糖”變量聲明形式都不支持變量的延遲初始化,因此對(duì)于這類局部變量,和包級(jí)變量一樣,我們只能采用通用的變量聲明形式:
var err error
5.2.2 第二類:對(duì)于聲明且顯式初始化的局部變量,建議使用短變量聲明形式
短變量聲明形式是局部變量最常用的聲明形式,它遍布在 Go 標(biāo)準(zhǔn)庫代碼中。對(duì)于接受默認(rèn)類型的變量,我們使用下面這種形式:
a := 17
f := 3.14
s := "hello, gopher!"
對(duì)于不接受默認(rèn)類型的變量,我們依然可以使用短變量聲明形式,只是在":="右側(cè)要做一個(gè)顯式轉(zhuǎn)型,以保持聲明的一致性:
a := int32(17)
f := float32(3.14)
s := []byte("hello, gopher!")
這里我們還要注意:盡量在分支控制時(shí)使用短變量聲明形式。
分支控制應(yīng)該是 Go 中短變量聲明形式應(yīng)用得最廣泛的場景了。在編寫 Go 代碼時(shí),我們很少單獨(dú)聲明用于分支控制語句中的變量,而是將它與 if、for 等控制語句通過短變量聲明形式融合在一起,即在控制語句中直接聲明用于控制語句代碼塊中的變量。
你看一下下面這個(gè)我摘自 Go 標(biāo)準(zhǔn)庫中的代碼,strings 包的 LastIndexAny 方法為我們很好地詮釋了如何將短變量聲明形式與分支控制語句融合在一起使用:
// $GOROOT/src/strings/strings.go
func LastIndexAny(s, chars string) int {if chars == "" {// Avoid scanning all of s.return -1}if len(s) > 8 {// 作者注:在if條件控制語句中使用短變量聲明形式聲明了if代碼塊中要使用的變量as和isASCIIif as, isASCII := makeASCIISet(chars); isASCII { for i := len(s) - 1; i >= 0; i-- {if as.contains(s[i]) {return i}}return -1}}for i := len(s); i > 0; { // 作者注:在for循環(huán)控制語句中使用短變量聲明形式聲明了for代碼塊中要使用的變量cr, size := utf8.DecodeLastRuneInString(s[:i])i -= sizefor _, c := range chars {if r == c {return i}}}return -1
}
而且,短變量聲明的這種融合的使用方式也體現(xiàn)出“就近”原則,讓變量的作用域最小化。
另外,雖然良好的函數(shù) / 方法設(shè)計(jì)都講究“單一職責(zé)”,所以每個(gè)函數(shù) / 方法規(guī)模都不大,很少需要應(yīng)用 var 塊來聚類聲明局部變量,但是如果你在聲明局部變量時(shí)遇到了適合聚類的應(yīng)用場景,你也應(yīng)該毫不猶豫地使用 var 聲明塊來聲明多于一個(gè)的局部變量,具體寫法你可以參考 Go 標(biāo)準(zhǔn)庫 net 包中 resolveAddrList 方法:
// $GOROOT/src/net/dial.go
func (r *Resolver) resolveAddrList(ctx context.Context, op, network, addr string, hint Addr) (addrList, error) {... ...var (tcp *TCPAddrudp *UDPAddrip *IPAddrwildcard bool)... ...
}
六、變量聲明小結(jié)
以下是變量聲明形象化的一副小圖:
你可以看到,良好的變量聲明實(shí)踐需要我們考慮多方面因素,包括明確要聲明的變量是包級(jí)變量還是局部變量、是否要延遲初始化、是否接受默認(rèn)類型、是否是分支控制變量并結(jié)合聚類和就近原則等。
七、常量
7.1 常量定義
相對(duì)于變量,常量是恒定不變的值,多用于定義程序運(yùn)行期間不會(huì)改變的那些值。 常量的聲明和變量聲明非常類似,只是把var
換成了const
,常量在定義的時(shí)候必須賦值。
const Pi float64 = 3.14159265358979323846 // 單行常量聲明
聲明了pi
常量之后,在整個(gè)程序運(yùn)行期間它們的值都不能再發(fā)生變化了。
所以,常量不能再重新賦值為其他的值。因此下面的程序?qū)⒉荒苷9ぷ?#xff0c;它將出現(xiàn)一個(gè)編譯錯(cuò)誤: cannot assign to a.
package mainfunc main() { const a = 55 // 允許a = 89 // 不允許重新賦值
}
7.2 聲明多個(gè)常量
多個(gè)常量也可以一起聲明:
// 以const代碼塊形式聲明常量
const (size int64 = 4096i, j, s = 13, 14, "bar" // 單行聲明多個(gè)常量
)
const同時(shí)聲明多個(gè)常量時(shí),如果省略了值則表示和上面一行的值相同。 例如:
const (n1 = 100n2n3
)
上面示例中,常量n1
、n2
、n3
的值都是100。
7.3 常量的創(chuàng)新點(diǎn)
7.3.1 無類型常量
Go 語言對(duì)類型安全是有嚴(yán)格要求的:即便兩個(gè)類型擁有著相同的底層類型,但它們?nèi)匀皇遣煌臄?shù)據(jù)類型,不可以被相互比較或混在一個(gè)表達(dá)式中進(jìn)行運(yùn)算。這一要求不僅僅適用于變量,也同樣適用于有類型常量(Typed Constant)中,你可以在下面代碼中看出這一點(diǎn):
type myInt int
const n myInt = 13
const m int = n + 5 // 編譯器報(bào)錯(cuò):cannot use n + 5 (type myInt) as type int in const initializerfunc main() {var a int = 5fmt.Println(a + n) // 編譯器報(bào)錯(cuò):invalid operation: a + n (mismatched types int and myInt)
}
那么在 Go 語言中,只有這一種方法能讓上面代碼編譯通過、正常運(yùn)行嗎 ?當(dāng)然不是,我們也可以使用 Go 中的無類型常量來實(shí)現(xiàn),你可以看看這段代碼:
type myInt int
const n = 13func main() {var a myInt = 5fmt.Println(a + n) // 輸出:18
}
你可以看到,在這個(gè)代碼中,常量 n 在聲明時(shí)并沒有顯式地被賦予類型,在 Go 中,這樣的常量就被稱為無類型常量(Untyped Constant),即不帶有明確類型的字面常量
不過,無類型常量也不是說就真的沒有類型,它也有自己的默認(rèn)類型,不過它的默認(rèn)類型是根據(jù)它的初值形式來決定的。像上面代碼中的常量 n 的初值為整數(shù)形式,所以它的默認(rèn)類型為 int。
7.3.2 隱式轉(zhuǎn)型
隱式轉(zhuǎn)型說的就是,對(duì)于無類型常量參與的表達(dá)式求值,Go 編譯器會(huì)根據(jù)上下文中的類型信息,把無類型常量自動(dòng)轉(zhuǎn)換為相應(yīng)的類型后,再參與求值計(jì)算,這一轉(zhuǎn)型動(dòng)作是隱式進(jìn)行的。但由于轉(zhuǎn)型的對(duì)象是一個(gè)常量,所以這并不會(huì)引發(fā)類型安全問題,Go 編譯器會(huì)保證這一轉(zhuǎn)型的安全性。
繼續(xù)以上面代碼為例來分析一下,Go 編譯器會(huì)自動(dòng)將 a+n 這個(gè)表達(dá)式中的常量 n 轉(zhuǎn)型為 myInt 類型,再與變量 a 相加。由于變量 a 的類型 myInt 的底層類型也是 int,所以這個(gè)隱式轉(zhuǎn)型不會(huì)有任何問題。
不過,如果 Go 編譯器在做隱式轉(zhuǎn)型時(shí),發(fā)現(xiàn)無法將常量轉(zhuǎn)換為目標(biāo)類型,Go 編譯器也會(huì)報(bào)錯(cuò),比如下面的代碼就是這樣:
const m = 1333333333var k int8 = 1
j := k + m // 編譯器報(bào)錯(cuò):constant 1333333333 overflows int8
這個(gè)代碼中常量 m 的值 1333333333 已經(jīng)超出了 int8 類型可以表示的范圍,所以我們將它轉(zhuǎn)換為 int8 類型時(shí),就會(huì)導(dǎo)致編譯器報(bào)溢出錯(cuò)誤。
從前面這些分析中,我們可以看到,無類型常量與常量隱式轉(zhuǎn)型的“珠聯(lián)璧合”使得在 Go 這樣的具有強(qiáng)類型系統(tǒng)的語言,在處理表達(dá)式混合數(shù)據(jù)類型運(yùn)算的時(shí)候具有比較大的靈活性,代碼編寫也得到了一定程度的簡化。也就是說,我們不需要在求值表達(dá)式中做任何顯式轉(zhuǎn)型了。所以說,在 Go 中,使用無類型常量是一種慣用法,你可以多多熟悉這種形式。
7.3.3 實(shí)現(xiàn)枚舉
Go 語言其實(shí)并沒有原生提供枚舉類型,但是我們可以使用 const
代碼塊定義的常量集合,來實(shí)現(xiàn)枚舉。這是因?yàn)?#xff0c;枚舉類型本質(zhì)上就是一個(gè)由有限數(shù)量常量所構(gòu)成的集合。不過,用 Go 常量實(shí)現(xiàn)枚舉可不是我們的臨時(shí)起意,而是 Go 設(shè)計(jì)者們的原創(chuàng),他們在語言設(shè)計(jì)之初就希望將枚舉類型與常量合二為一,這樣就不需要再單獨(dú)提供枚舉類型了。
使用Go 實(shí)現(xiàn)枚舉,分解成了 Go 中的兩個(gè)特性:自動(dòng)重復(fù)上一行,以及引入 const 塊中的行偏移量指示器 iota,這樣它們就可以分別獨(dú)立使用了。
接下來我們逐一看看這兩個(gè)特性。首先,**Go 的 const 語法提供了“隱式重復(fù)前一個(gè)非空表達(dá)式”的機(jī)制,**比如下面代碼:
const (Apple, Banana = 11, 22Strawberry, Grape Pear, Watermelon
)
這個(gè)代碼里,常量定義的后兩行并沒有被顯式地賦予初始值,所以 Go 編譯器就為它們自動(dòng)使用上一行的表達(dá)式,也就獲得了下面這個(gè)等價(jià)的代碼:
const (Apple, Banana = 11, 22Strawberry, Grape = 11, 22 // 使用上一行的初始化表達(dá)式Pear, Watermelon = 11, 22 // 使用上一行的初始化表達(dá)式
)
不過,僅僅是重復(fù)上一行顯然無法滿足“枚舉”的要求,因?yàn)槊杜e類型中的每個(gè)枚舉常量的值都是唯一的。所以,Go 在這個(gè)特性的基礎(chǔ)上又提供了“神器”:iota,有了 iota,我們就可以定義滿足各種場景的枚舉常量了。
iota 介紹
iota
是Go 語言的一個(gè)預(yù)定義標(biāo)識(shí)符-常量計(jì)數(shù)器,只能在常量的表達(dá)式中使用。
iota 表示的是 const 聲明塊(包括單行聲明)中,每個(gè)常量所處位置在塊中的偏移值(從零開始)。同時(shí),每一行中的 iota 自身也是一個(gè)無類型常量,可以像前面我們提到的無類型常量那樣,自動(dòng)參與到不同類型的求值過程中來,不需要我們再對(duì)它進(jìn)行顯式轉(zhuǎn)型操作。
可以看看下面這個(gè) Go 標(biāo)準(zhǔn)庫中 sync/mutex.go
中的一段基于 iota 的枚舉常量的定義:
// $GOROOT/src/sync/mutex.go
const ( mutexLocked = 1 << iotamutexWokenmutexStarvingmutexWaiterShift = iotastarvationThresholdNs = 1e6
)
首先,這個(gè) const 聲明塊的第一行是 mutexLocked = 1 << iota ,iota 的值是這行在 const 塊中的偏移,因此 iota 的值為 0,我們得到 mutexLocked 這個(gè)常量的值為 1 << 0,也就是 1。
接著,第二行:mutexWorken 。因?yàn)檫@個(gè) const 聲明塊中并沒有顯式的常量初始化表達(dá)式,所以我們根據(jù) const 聲明塊里“隱式重復(fù)前一個(gè)非空表達(dá)式”的機(jī)制,這一行就等價(jià)于 mutexWorken = 1 << iota。而且,又因?yàn)檫@一行是 const 塊中的第二行,所以它的偏移量 iota 的值為 1,我們得到 mutexWorken 這個(gè)常量的值為 1 << 1,也就是 2。
然后是 mutexStarving。這個(gè)常量同 mutexWorken 一樣,這一行等價(jià)于 mutexStarving = 1 << iota。而且,也因?yàn)檫@行的 iota 的值為 2,我們可以得到 mutexStarving 這個(gè)常量的值為 1 << 2,也就是 4;
再然后我們看 mutexWaiterShift = iota 這一行,這一行為常量 mutexWaiterShift 做了顯式初始化,這樣就不用再重復(fù)前一行了。由于這一行是第四行,而且作為行偏移值的 iota 的值為 3,因此 mutexWaiterShift 的值就為 3。
而最后一行,代碼中直接用了一個(gè)具體值 1e6 給常量 starvationThresholdNs 進(jìn)行了賦值,那么這個(gè)常量值就是 1e6 本身了。
看完這個(gè)例子的分析,我相信你對(duì)于 iota 就會(huì)有更深的理解了。不過我還要提醒你的是,位于同一行的 iota 即便出現(xiàn)多次,多個(gè) iota 的值也是一樣的,比如下面代碼:
const (Apple, Banana = iota, iota + 10 // 0, 10 (iota = 0)Strawberry, Grape // 1, 11 (iota = 1)Pear, Watermelon // 2, 12 (iota = 2)
)
我們以第一組常量 Apple 與 Banana 為例分析一下,它們分為被賦值為 iota 與 iota+10,而且由于這是 const 常量聲明塊的第一行,因此兩個(gè) iota 的值都為 0,于是就有了“Apple=0, Banana=10”的結(jié)果。下面兩組變量分析過程也是類似的,你可以自己試一下。
如果我們要略過 iota = 0,從 iota = 1 開始正式定義枚舉常量,我們可以效仿下面標(biāo)準(zhǔn)庫中的代碼:
// $GOROOT/src/syscall/net_js.go
const (_ = iotaIPV6_V6ONLY // 1SOMAXCONN // 2SO_ERROR // 3
)
在這個(gè)代碼里,我們使用了空白標(biāo)識(shí)符作為第一個(gè)枚舉常量,它的值就是 iota。雖然它本身沒有實(shí)際意義,但后面的常量值都會(huì)重復(fù)它的初值表達(dá)式(這里是 iota),于是我們真正的枚舉常量值就從 1 開始了。
那如果我們的枚舉常量值并不連續(xù),而是要略過某一個(gè)或幾個(gè)值,又要怎么辦呢?我們也可以借助空白標(biāo)識(shí)符來實(shí)現(xiàn),如下面這個(gè)代碼:
const (_ = iota // 0Pin1Pin2Pin3_Pin5 // 5
)
以看到,在上面這個(gè)枚舉定義中,枚舉常量集合中沒有 Pin4。為了略過 Pin4,我們在它的位置上使用了空白標(biāo)識(shí)符。
這樣,Pin5 就會(huì)重復(fù) Pin3,也就是向上數(shù)首個(gè)不為空的常量標(biāo)識(shí)符的值,這里就是 iota,而且由于它所在行的偏移值為 5,因此 Pin5 的值也為 5,這樣我們成功略過了 Pin4 這個(gè)枚舉常量以及 4 這個(gè)枚舉值。
而且,iota 特性讓我們維護(hù)枚舉常量列表變得更加容易。比如我們使用傳統(tǒng)的枚舉常量聲明方式,來聲明一組按首字母排序的“顏色”常量,也就是這樣:
const ( Black = 1 Red = 2Yellow = 3
)
假如這個(gè)時(shí)候,我們要增加一個(gè)新顏色 Blue。那根據(jù)字母序,這個(gè)新常量應(yīng)該放在 Red 的前面呀。但這樣一來,我們就需要像下面代碼這樣將 Red 到 Yellow 的常量值都手動(dòng)加 1,十分費(fèi)力。
const (Blue = 1Black = 2Red = 3Yellow = 4
)
那如果我們使用 iota 重新定義這組“顏色”枚舉常量是不是可以更方便呢?我們可以像下面代碼這樣試試看:
const (_ = iota BlueRed Yellow
)
這樣,無論后期我們需要增加多少種顏色,我們只需將常量名插入到對(duì)應(yīng)位置就可以,其他就不需要再做任何手工調(diào)整了。
而且,如果一個(gè) Go 源文件中有多個(gè) const 代碼塊定義的不同枚舉,每個(gè) const 代碼塊中的 iota 也是獨(dú)立變化的,也就是說,每個(gè) const 代碼塊都擁有屬于自己的 iota,如下面代碼所示:
const (a = iota + 1 // 1, iota = 0b // 2, iota = 1c // 3, iota = 2
)const (i = iota << 1 // 0, iota = 0j // 2, iota = 1k // 4, iota = 2
)
你可以看到,每個(gè) iota 的生命周期都始于一個(gè) const 代碼塊的開始,在該 const 代碼塊結(jié)束時(shí)結(jié)束。
幾個(gè)常見的iota
示例:
使用_
跳過某些值
const (n1 = iota //0n2 //1_n4 //3)
iota
聲明中間插隊(duì)
const (n1 = iota //0n2 = 100 //100n3 = iota //2n4 //3)
const n5 = iota //0
定義數(shù)量級(jí) (這里的<<
表示左移操作,1<<10
表示將1的二進(jìn)制表示向左移10位,也就是由1
變成了10000000000
,也就是十進(jìn)制的1024。同理2<<2
表示將2的二進(jìn)制表示向左移2位,也就是由10
變成了1000
,也就是十進(jìn)制的8。)
const (_ = iotaKB = 1 << (10 * iota) // i<<10MB = 1 << (10 * iota) // i<<20GB = 1 << (10 * iota)TB = 1 << (10 * iota)PB = 1 << (10 * iota))
多個(gè)iota
定義在一行
const (a, b = iota + 1, iota + 2 //1,2c, d //2,3e, f //3,4)