天天看點

Go開發 之 基礎文法(常量、枚舉、注釋、類型别名、指針)

文章目錄

  • ​​1、常量(const關鍵字)​​
  • ​​1.1、概念​​
  • ​​1.2、iota 常量生成器​​
  • ​​1.3、無類型常量​​
  • ​​2、枚舉(const和iota枚舉)​​
  • ​​2.1、概念​​
  • ​​2.2、将枚舉值轉換為字元串​​
  • ​​3、注釋(定義及使用)​​
  • ​​3.1、定義​​
  • ​​3.2、示例​​
  • ​​3.3、godoc 工具​​
  • ​​4、類型别名(type關鍵字)​​
  • ​​4.1、區分類型别名與類型定義​​
  • ​​4.2、非本地類型不能定義方法​​
  • ​​4.3、在結構體成員嵌入時使用别名​​
  • ​​5、指針​​
  • ​​5.1、概念​​
  • ​​5.1.1、Go的指針​​
  • ​​5.1.2、C/C++中的指針​​
  • ​​5.2、認識指針位址和指針類型​​
  • ​​5.3、從指針擷取指針指向的值​​
  • ​​5.4、使用指針修改值​​
  • ​​5.5、建立指針的另一種方法——new() 函數​​
  • ​​5.6、示例:使用指針變量擷取指令行的輸入資訊​​

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下的項目:

Go開發 之 基礎文法(常量、枚舉、注釋、類型别名、指針)

舉例看一下某一個項目的doc,如下:

Go開發 之 基礎文法(常量、枚舉、注釋、類型别名、指針)

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​

​ 結果:

Go開發 之 基礎文法(常量、枚舉、注釋、類型别名、指針)