1、常量(const關鍵字)
1.1、概念
Go語言中的常量使用關鍵字 const 定義,用於存儲不會改變的數據,常量是在編譯時被創建的,即使定義在函數內部也是如此,並且只能是布爾型、數字型(整數型、浮點型和複數)和字符串型。由於編譯時的限制,定義常量的表達式必須爲能被編譯器求值的常量表達式。
常量的定義格式和變量的聲明語法類似:const name [type] = value
,例如:
const pi = 3.14159 // 相當於 math.Pi 的近似值
在Go語言中,你可以省略類型說明符 [type],因爲編譯器可以根據變量的值來推斷其類型。
- 顯式類型定義: const b string = “abc”
- 隱式類型定義: const b = “abc”
常量的值必須是能夠在編譯時就能夠確定的,可以在其賦值表達式中涉及計算過程,但是所有用於計算的值必須在編譯期間就能獲得。
- 正確的做法:const c1 = 2/3
- 錯誤的做法:const c2 = getNumber() // 引發構建錯誤: getNumber() 用做值
可以批量生成,例如:
const (
a = 1
b
c = 2
d
)
fmt.Println(a, b, c, d) // "1 1 2 2"
如果只是簡單地複製右邊的常量表達式,其實並沒有太實用的價值。但是它可以帶來其它的特性,那就是 iota 常量生成器語法。
1.2、iota 常量生成器
常量聲明可以使用 iota 常量生成器初始化,它用於生成一組以相似規則初始化的常量,但是不用每行都寫一遍初始化表達式。在一個 const 聲明語句中,在第一個聲明的常量所在的行,iota 將會被置爲 0,然後在每一個有常量聲明的行加一。示例:
type Weekday int
const (
Sunday Weekday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)
週日將對應 0,週一爲 1,以此類推。
1.3、無類型常量
Go語言的常量有個不同尋常之處。雖然一個常量可以有任意一個確定的基礎類型,例如 int 或 float64,或者是類似 time.Duration 這樣的基礎類型,但是許多常量並沒有一個明確的基礎類型。
編譯器爲這些沒有明確的基礎類型的數字常量提供比基礎類型更高精度的算術運算,可以認爲至少有 256bit 的運算精度。這裏有六種未明確類型的常量類型,分別是無類型的布爾型、無類型的整數、無類型的字符、無類型的浮點數、無類型的複數、無類型的字符串。
通過延遲明確常量的具體類型,不僅可以提供更高的運算精度,而且可以直接用於更多的表達式而不需要顯式的類型轉換。
math.Pi 無類型的浮點數常量,可以直接用於任意需要浮點數或複數的地方:
var x float32 = math.Pi
var y float64 = math.Pi
var z complex128 = math.Pi
如果 math.Pi 被確定爲特定類型,比如 float64,那麼結果精度可能會不一樣,同時對於需要 float32 或 complex128 類型值的地方則需要一個明確的強制類型轉換:
const Pi64 float64 = math.Pi
var x float32 = float32(Pi64)
var y float64 = Pi64
var z complex128 = complex128(Pi64)
對於常量面值,不同的寫法可能會對應不同的類型。例如 0、0.0、0i 和 \u0000 雖然有着相同的常量值,但是它們分別對應無類型的整數、無類型的浮點數、無類型的複數和無類型的字符等不同的常量類型。同樣,true 和 false 也是無類型的布爾類型,字符串面值常量是無類型的字符串類型。
2、枚舉(const和iota枚舉)
2.1、概念
Go語言現階段沒有枚舉類型,但是可以使用 const 常量的 iota 來模擬枚舉類型,如下:
type Weapon int
const (
Arrow Weapon = iota // 開始生成枚舉值, 默認爲0
Shuriken
SniperRifle
Rifle
Blower
)
// 輸出所有枚舉值
fmt.Println(Arrow, Shuriken, SniperRifle, Rifle, Blower)
// 使用枚舉類型並賦初值
var weapon Weapon = Blower
fmt.Println(weapon)
代碼輸出如下:
0 1 2 3 4
4
iota高級用法,如:
const (
FlagNone = 1 << iota
FlagRed
FlagGreen
FlagBlue
)
fmt.Printf("%d %d %d\n", FlagRed, FlagGreen, FlagBlue) // 2 4 8
fmt.Printf("%b %b %b\n", FlagRed, FlagGreen, FlagBlue) // 10 100 1000
2.2、將枚舉值轉換爲字符串
直接看代碼:
package main
import "fmt"
// 聲明芯片類型
type ChipType int
const (
None ChipType = iota
CPU // 中央處理器
GPU // 圖形處理器
)
func (c ChipType) String() string {
switch c {
case None:
return "None"
case CPU:
return "CPU"
case GPU:
return "GPU"
}
return "N/A"
}
func main() {
// 輸出CPU的值並以整型格式顯示
fmt.Printf("%s %d", CPU, CPU) // CPU 1
}
3、註釋(定義及使用)
3.1、定義
Go語言的註釋和C/C++的註釋一樣。主要分成兩類,分別是單行註釋和多行註釋。
- 單行註釋簡稱行註釋,是最常見的註釋形式,可以在任何地方使用以//開頭的單行註釋;
- 多行註釋簡稱塊註釋,以/開頭,並以/結尾,且不可以嵌套使用,多行註釋一般用於包的文檔描述或註釋成塊的代碼片段。
3.2、示例
單行註釋的格式如下所示
//單行註釋
多行註釋的格式如下所示
/*
第一行註釋
第二行註釋
...
*/
3.3、godoc 工具
godoc 工具會從 Go 程序和包文件中提取頂級聲明的首行註釋以及每個對象的相關注釋,並生成相關文檔,也可以作爲一個提供在線文檔瀏覽的 web 服務器,Go語言官網(https://golang.google.cn/)就是通過這種形式實現的。
go get 命令來獲取 godoc 工具。如果golang的牆太厚,可以用github:“https://github.com/golang/tools.git”
go get golang.org/x/tools/cmd/godoc
可以直接使用,在命令行輸入:godoc -http=:6060
在瀏覽器輸入“http://localhost:6060/pkg/”,可以看到你所以Gopath目錄下的src下的項目:
舉例看一下某一個項目的doc,如下:
4、類型別名(type關鍵字)
類型別名是 Go 1.9 版本添加的功能,主要用於解決代碼升級、遷移中存在的類型兼容性問題。
在 Go 1.9 版本之前定義內建類型的代碼是這樣寫的:
type byte uint8
type rune int32
而在 Go 1.9 版本之後變爲:
type byte = uint8
type rune = int32
這個修改就是配合類型別名而進行的修改。
4.1、區分類型別名與類型定義
定義類型別名的寫法爲:type TypeAlias = Type
。類型別名規定:TypeAlias 只是 Type 的別名,本質上 TypeAlias 與 Type 是同一個類型。示例代碼:
package main
import (
"fmt"
)
// 將NewInt定義爲int類型
type NewInt int
// 將int取一個別名叫IntAlias
type IntAlias = int
func main() {
// 將a聲明爲NewInt類型
var a NewInt
// 查看a的類型名
fmt.Printf("a type: %T\n", a)
// 將a2聲明爲IntAlias類型
var a2 IntAlias
// 查看a2的類型名
fmt.Printf("a2 type: %T\n", a2)
}
結果:
a type: main.NewInt
a2 type: int
結果顯示 a 的類型是 main.NewInt,表示 main 包下定義的 NewInt 類型,a2 類型是 int,IntAlias 類型只會在代碼中存在,編譯完成時,不會有 IntAlias 類型。
4.2、非本地類型不能定義方法
能夠隨意地爲各種類型起名字,並不意味着可以在自己包裏爲這些類型任意添加方法,看代碼:
package main
import (
"time"
)
// 定義time.Duration的別名爲MyDuration
type MyDuration = time.Duration
... ...
這樣的代碼,編譯會報錯,
cannot define new methods on non-local type time.Duration
解決方案有兩種:
- 1、將第 8 行修改爲 type MyDuration time.Duration,也就是將 MyDuration 從別名改爲類型;
- 2、將 MyDuration 的別名定義放在 time 包中。
4.3、在結構體成員嵌入時使用別名
當類型別名作爲結構體嵌入的成員時,情況如下:
package main
import (
"fmt"
"reflect"
)
// 定義商標結構
type Brand struct {
}
// 爲商標結構添加Show()方法
func (t Brand) Show() {
}
// 爲Brand定義一個別名FakeBrand
type FakeBrand = Brand
// 定義車輛結構
type Vehicle struct {
// 嵌入兩個結構
FakeBrand
Brand
}
func main() {
// 聲明變量a爲車輛類型
var a Vehicle
// 指定調用FakeBrand的Show
a.FakeBrand.Show()
// 取a的類型反射對象
ta := reflect.TypeOf(a)
// 遍歷a的所有成員
for i := 0; i < ta.NumField(); i++ {
// a的成員信息
f := ta.Field(i)
// 打印成員的字段名和類型
fmt.Printf("FieldName: %v, FieldType: %v\n", f.Name, f.Type.
Name())
}
}
結果如下:
FieldName: FakeBrand, FieldType: Brand
FieldName: Brand, FieldType: Brand
5、指針
5.1、概念
5.1.1、Go的指針
Go語言爲程序員提供了控制數據結構指針的能力,但並不能進行指針運算。Go語言允許你控制特定集合的數據結構、分配的數量以及內存訪問模式,這對於構建運行良好的系統是非常重要的。指針對於性能的影響不言而喻,如果你想要做系統編程、操作系統或者網絡應用,指針更是不可或缺的一部分。
指針在Go語言中可以被拆分爲兩個核心概念:
- 類型指針,允許對這個指針類型的數據進行修改,傳遞數據可以直接使用指針,無須拷貝數據,類型指針不能進行偏移和運算。
- 切片,由指向起始元素的原始指針、元素數量和容量組成。
受益於這樣的約束和拆分,Go語言的指針類型變量即擁有指針高效訪問的特點,又不會發生指針偏移,從而避免了非法修改關鍵性數據的問題。同時,垃圾回收也比較容易對不會發生偏移的指針進行檢索和回收。
切片比原始指針具備更強大的特性,而且更爲安全。切片在發生越界時,運行時會報出宕機,並打出堆棧,而原始指針只會崩潰。
5.1.2、C/C++中的指針
說到 C/C++ 中的指針,會讓許多人談虎色變,尤其是對指針的偏移、運算和轉換。
其實,指針是 C/C++ 語言擁有極高性能的根本所在,在操作大塊數據和做偏移時即方便又便捷。因此,操作系統依然使用C語言及指針的特性進行編寫。
C/C++ 中指針飽受詬病的根本原因是指針的運算和內存釋放,C/C++ 語言中的裸指針可以自由偏移,甚至可以在某些情況下偏移進入操作系統的核心區域,我們的計算機操作系統經常需要更新、修復漏洞的本質,就是爲解決指針越界訪問所導致的“緩衝區溢出”的問題。
要明白指針,需要知道幾個概念:指針地址、指針類型和指針取值,下面將展開詳細說明。
5.2、認識指針地址和指針類型
一個指針變量可以指向任何一個值的內存地址,它所指向的值的內存地址在 32 和 64 位機器上分別佔用 4 或 8 個字節,佔用字節的大小與所指向的值的大小無關。當一個指針被定義後沒有分配到任何變量時,它的默認值爲 nil。指針變量通常縮寫爲 ptr。
每個變量在運行時都擁有一個地址,這個地址代表變量在內存中的位置。Go語言中使用在變量名前面添加&操作符(前綴)來獲取變量的內存地址(取地址操作),格式:
ptr := &v // v 的類型爲 T
其中 v 代表被取地址的變量,變量 v 的地址使用變量 ptr 進行接收,ptr 的類型爲*T,稱做 T 的指針類型,*代表指針。
舉例:
package main
import (
"fmt"
)
func main() {
var cat int = 1
var str string = "banana"
fmt.Printf("%p %p", &cat, &str)
}
結果:
0xc042052088 0xc0420461b0
提示:變量、指針和地址三者的關係是,每個變量都擁有地址,指針的值就是地址。
5.3、從指針獲取指針指向的值
當使用&操作符對普通變量進行取地址操作並得到變量的指針後,可以對指針使用*操作符,也就是指針取值,代碼如下:
package main
import (
"fmt"
)
func main() {
// 準備一個字符串類型
var house = "Malibu Point 10880, 90265"
// 對字符串取地址, ptr類型爲*string
ptr := &house
// 打印ptr的類型
fmt.Printf("ptr type: %T\n", ptr)
// 打印ptr的指針地址
fmt.Printf("address: %p\n", ptr)
// 對指針進行取值操作
value := *ptr
// 取值後的類型
fmt.Printf("value type: %T\n", value)
// 指針取值後就是指向變量的值
fmt.Printf("value: %s\n", value)
}
結果:
ptr type: *string
address: 0xc0420401b0
value type: string
value: Malibu Point 10880, 90265
取地址操作符&和取值操作符*是一對互補操作符,&取出地址,*根據地址取出地址指向的值。
變量、指針地址、指針變量、取地址、取值的相互關係和特性如下:
- 對變量進行取地址操作使用&操作符,可以獲得這個變量的指針變量。
- 指針變量的值是指針地址。
- 對指針變量進行取值操作使用*操作符,可以獲得指針變量指向的原變量的值。
5.4、使用指針修改值
通過指針不僅可以取值,也可以修改值。
前面已經演示了使用多重賦值的方法進行數值交換,使用指針同樣可以進行數值交換,代碼如下:
package main
import "fmt"
// 交換函數
func swap(a, b *int) {
// 取a指針的值, 賦給臨時變量t
t := *a
// 取b指針的值, 賦給a指針指向的變量
*a = *b
// 將a指針的值賦給b指針指向的變量
*b = t
}
func main() {
// 準備兩個變量, 賦值1和2
x, y := 1, 2
// 交換變量值
swap(&x, &y)
// 輸出變量值
fmt.Println(x, y)
}
運行結果:
2 1
*操作符作爲右值時,意義是取指針的值,作爲左值時,也就是放在賦值操作符的左邊時,表示 a 指針指向的變量。其實歸納起來,*操作符的根本意義就是操作指針指向的變量。當操作在右值時,就是取指向變量的值,當操作在左值時,就是將值設置給指向的變量。代碼如下:
package main
import "fmt"
func swap(a, b *int) {
b, a = a, b
}
func main() {
x, y := 1, 2
swap(&x, &y)
fmt.Println(x, y)
}
結果:
1 2
結果表明,交換是不成功的。上面代碼中的 swap() 函數交換的是 a 和 b 的地址,在交換完畢後,a 和 b 的變量值確實被交換。但和 a、b 關聯的兩個變量並沒有實際關聯。這就像寫有兩座房子的卡片放在桌上一字攤開,交換兩座房子的卡片後並不會對兩座房子有任何影響。
5.5、創建指針的另一種方法——new() 函數
Go語言還提供了另外一種方法來創建指針變量,格式:new(類型)
一般這樣寫:
str := new(string)
*str = "Go語言教程"
fmt.Println(*str)
new() 函數可以創建一個對應類型的指針,創建過程會分配內存,被創建的指針指向默認值。
5.6、示例:使用指針變量獲取命令行的輸入信息
Go語言內置的 flag 包實現了對命令行參數的解析,flag 包使得開發命令行工具更爲簡單。
提前定義一些命令行指令和對應的變量,並在運行時輸入對應的參數,經過 flag 包的解析後即可獲取命令行的數據。
package main
// 導入系統包
import (
"flag"
"fmt"
)
// 定義命令行參數
var mode = flag.String("mode", "", "process mode")
func main() {
// 解析命令行參數
flag.Parse()
// 輸出命令行參數
fmt.Println(*mode)
}
運行:go run main.go --mode=fast
結果:
原理: