剖析 golang 的25個關鍵字
基本在所有語言當中,關鍵字都是不允許用於自定義的,在Golang中有25個關鍵字,圖示如下:
下面我們逐個解析這25個關鍵字。
var && const
使用var關鍵字是Go最基本的定義變數方式,有時也會使用到 := 來定義變數。
定義變數
var name string
複製程式碼
定義變數並初始化值
var name string = "keywords"
複製程式碼
同時初始化多個同型別變數
var name1, name2, name3 string = "name1", "name2", "name3"
複製程式碼
同時初始化多個不同型別變數
var (
name string = "keywords",
count int = 2
)
複製程式碼
也可省略變數型別
var name1, name2, name3 = "name1", "name2", "name3"
複製程式碼
使用 := 這個符號取代var和type,這種形式叫做簡短宣告。不過它有一個限制,那就是它只能用在函式內部;在函式外部使用則會無法編譯通過,所以一般用var方式來定義全域性變數。
name1, name2, name3 := "name1", "name2", "name3"
複製程式碼
const 用來宣告一個常量,const 語句可以出現在任何 var 語句可以出現的地方,宣告常量方式與 var 相同,這裡就不在贅述了。但是需要注意的是,const 宣告的是常量,一旦建立,不可賦值修改。
package && import
包是函式和資料的集合。用 package 關鍵字定義一個包,用 import 關鍵字引入一個包,檔名不需要和包名一致。包名的約定是使用小寫字元。Go 包可以由多個檔案組成,但是使用相同的 package 這一行。
當函式或者變數等首字母為大寫時,會被匯出,可在外部直接呼叫。
包名是匯入的預設名稱。可以通過在匯入語句指定其他名稱來覆蓋預設名稱
import bar "bytes"
複製程式碼
map
map 是 Go 內建關聯資料型別(在一些其他的語言中稱為雜湊 或者字典 )。
建立一個空 map
m := make(map[string]int)
複製程式碼
設定鍵值對對map賦值
m["k"] = 7
複製程式碼
使用 name[key] 來獲取一個鍵的值
v := m["k"]
fmt.Println("v: ", v)
複製程式碼
當對一個 map 呼叫內建的 len 時,返回的是鍵值對數目
fmt.Println("len:", len(m))
複製程式碼
內建的 delete 可以從一個 map 中移除鍵值對
delete(m, "k")
複製程式碼
當從一個 map 中取值時,可選的第二返回值指示這個鍵是在這個 map 中。這可以用來消除鍵不存在和鍵有零值,像 0 或者 “” 而產生的歧義。
val, ok := m["k"]
fmt.Println("val:", val)
複製程式碼
如果想更深入瞭解map實現原理的同學,可檢視我得這篇小結 Array、Slice、Map原理淺析
func
使用關鍵字 func 定義函式
func test(a, b int) int {
return a + b
}
複製程式碼
其中,有返回值的函式,必須有明確的終止語句,否則會引發編譯錯誤。
函式是可變參的,變參的本質上是slice,只能有一個,且必須是最後一個,如
func test(s string, n ...int) string {
var x int
for _, i := range n {
x += i
}
return fmt.Sprintf(s, x)
}
複製程式碼
Golang 函式支援多返回值。這個特性在 Go 語言中經常被用到,例如用來同時返回一個函式的結果和錯誤資訊。
func vals() (int, int) {
return 3, 7
}
複製程式碼
return && defer
defer用於資源的釋放,會在函式返回之前進行呼叫。一般採用如下模式:
func test() {
f, err := os.Open(filename)
if err != nil {
panic(err)
}
defer f.Close()
}
複製程式碼
如果有多個defer表示式,呼叫順序類似於棧,越後面的defer表示式越先被呼叫,即後入先出的規則。
func test() {
defer fmt.Println(1)
defer fmt.Println(2)
}
複製程式碼
輸出結果為
2
1
複製程式碼
為了更深刻理解 defer 和 return 下面我們先來看幾個例子。
例1:
func f() (result int) {
defer func() {
result++
}()
return 0
}
複製程式碼
例2:
func f() (r int) {
t := 5
defer func() {
t = t + 5
}()
return t
}
複製程式碼
例3:
func f() (r int) {
defer func(r int) {
r = r + 5
}(r)
return 1
}
複製程式碼
函式返回的過程是這樣的:先給返回值賦值,然後呼叫defer表示式,最後才是返回到呼叫函式中。
defer表示式可能會在設定函式返回值之後,在返回到呼叫函式之前,修改返回值,使最終的函式返回值與你想象的不一致。
其實使用defer時,用一個簡單的轉換規則改寫一下,就不會迷糊了。改寫規則是將return語句拆成兩句寫,return xxx會被改寫成:
返回值 = xxx
呼叫defer函式
空的return
複製程式碼
下面我們針對上面的三個例子分析,先看例1,它可以改寫成這樣:
func f() (result int) {
result = 0 //return語句不是一條原子呼叫,return xxx其實是賦值+ret指令
func() { //defer被插入到return之前執行,也就是賦返回值和ret指令之間
result++
}()
return
}
複製程式碼
所以這個返回值是1。
例2,它可以改寫成這樣:
func f() (r int) {
t := 5
r = t //賦值指令
func() { //defer被插入到賦值與返回之間執行,這個例子中返回值r沒被修改過
t = t + 5
}
return //空的return指令
}
複製程式碼
所以這個返回值是5。
例3,它可以改寫成這樣:
func f() (r int) {
r = 1 //給返回值賦值
func(r int) { //這裡改的r是傳值傳進去的r,不會改變要返回的那個r值
r = r + 5
}(r)
return //空的return
}
複製程式碼
所以這個返回值是1。
defer確實是在return之前呼叫的。但表現形式上卻可能不像。本質原因是return xxx語句並不是一條原子指令,defer被插入到了賦值 與 ret之間,因此可能有機會改變最終的返回值。
goroutine的控制結構中,有一張表記錄defer,呼叫runtime.deferproc時會將需要defer的表示式記錄在表中,而在呼叫runtime.deferreturn的時候,則會依次從defer表中出棧並執行。
type
type是go語法裡的重要而且常用的關鍵字,其主要作用就是用來定義型別。
定義結構體
type Person struct {
name string
}
複製程式碼
型別等價定義,相當於型別重新命名
type name string
func main() {
var myname name = "golang" //其實就是字串型別
fmt.Println(myname)
}
複製程式碼
定義介面
type Person interface {
Run()
}
複製程式碼
struct
Go 的結構體 是各個欄位欄位的型別的集合。是值型別,賦值和傳參會賦值全部內容。
struct的基本用法
type Person struct {
Name string
Age int
}
func main() {
p := Person{
Name: "ck",
Age: 20,
}
p.Age = 25
fmt.Println(p)
}
複製程式碼
順序初始化必須包含全部欄位。否則會報錯
type Person struct {
Name string
Age int
}
func main() {
p1 := Person{"ck", 20}
p2 := Person{"ck"} // Error: too few values in struct initializer
}
複製程式碼
支援匿名結構,可用作結構成員或定義變數
type Person struct {
Name string
Attr struct{
age int
}
}
複製程式碼
支援 “==”、”!=” 相等操作符,可用作 map 鍵型別。
type Person struct {
Name string
}
m := map[Person]int{
Person{"ck"}: 10,
}
複製程式碼
struct 支援嵌入式結構,可以像普通欄位那樣訪問匿名欄位成員,如下
type Person struct {
Name string
}
type User struct {
Person
Age int
}
func main() {
u := User{
Person: Person{
Name: "ck",
},
Age: 22,
}
fmt.Println(u.Name) // ck
}
複製程式碼
當被嵌入結構中的某個欄位與當前struct中已存在的欄位同名時,編譯器從外向內逐級查詢所有層次的匿名欄位,直到發現目標或者報錯。
type Person struct {
Age int
}
type User struct {
Person
Age int
}
func main() {
u := User{
Person: Person{
Age: 20,
},
Age: 22,
}
fmt.Println(u.Age) // 22
}
複製程式碼
如果想訪問被嵌入結構Person中的Age
fmt.Println(u.Person.Age) // 20
複製程式碼
interface
首先 interface 是一種型別,從它的定義可以看出來用了 type 關鍵字,更準確的說 interface 是一種具有一組方法的型別,這些方法定義了 interface 的行為。
如果一個型別實現了一個 interface 中所有方法,我們說型別實現了該 interface,所以所有型別都實現了 empty interface,因為任何一種型別至少實現了 0 個方法。go 沒有顯式的關鍵字用來實現 interface,只需要實現 interface 包含的方法即可。
介面定義與基本操作
type Dog interface {
Category()
}
type Ha struct {
Name string
}
func (h Ha) Category() {
fmt.Println("狗子")
}
func main() {
h := Ha{"二哈"}
h.Category()
test(h)
}
func test(a Dog) {
fmt.Println("成功呼叫啦")
}
// 輸出結果為:狗子 成功呼叫啦
複製程式碼
上述程式碼中可以看到,對於 test 函式接收的引數型別為 Dog 這個型別,我們傳入的是 Ha 型別的h,該函式正常執行並輸出了結果,說明 Ha 型別已經成功實現了 Dog 。
嵌入結構
當我們需要使用複雜結構關係的時候,我們就會需要用到嵌入介面。接下來,我們將上述例子修改一下,如下所示
type Dog interface {
Animal
}
type Animal interface {
Category()
}
type Ha struct {
Name string
}
func (h Ha) Category() {
fmt.Println("狗子")
}
func main() {
h := Ha{"二哈"}
h.Category()
test(h)
}
func test(a Dog) {
fmt.Println("成功呼叫啦")
}
// 輸出結果為:狗子 成功呼叫啦
複製程式碼
可以看到,程式同樣正常執行,這也就證明了我們成功是實現了嵌入介面。
型別斷言
一個 interface 被多種型別實現時,有時候我們需要區分 interface 的變數究竟儲存哪種型別的值。
go 可以使用 comma, ok 的形式做區分 value, ok := em.(T):em 是 interface 型別的變數,T代表要斷言的型別,value 是 interface 變數儲存的值,ok 是 bool 型別表示是否為該斷言的型別 T。。
type Dog interface {
Animal
}
type Animal interface {
Category()
}
type Ha struct {
Name string
}
func (h Ha) Category() {
fmt.Println("狗子")
}
func main() {
h := Ha{"二哈"}
h.Category()
test(h)
}
func test(a Dog) {
if v, ok := a.(Ha); ok {
fmt.Println(v.Name)
}
}
// 輸出結果為:狗子 二哈
複製程式碼
如果需要區分多種型別,可以使用 switch 斷言,更簡單直接,這種斷言方式只能在 switch 語句中使用。
type Dog interface {
Animal
}
type Animal interface {
Category()
}
type Ha struct {
Name string
}
func (h Ha) Category() {
fmt.Println("狗子")
}
func main() {
h := Ha{"二哈"}
h.Category()
test(h)
}
func test(a Dog) {
switch v := a.(type) {
case Ha:
fmt.Println(v.Name)
default:
fmt.Println("暫未匹配到該型別")
}
}
// 輸出結果為:狗子 二哈
複製程式碼
空介面
空介面 interface{} 沒有任何方法簽名,也就意味著任何型別都實現了空介面。其作用類似於面嚮物件語言中的根物件 Object 。
func Print(v interface{}) {
fmt.Println(v)
}
func main() {
Print(1)
Print("Hello, World")
}
// 輸出結果為:1 Hello, World
複製程式碼
既然空的 interface 可以接受任何型別的引數,那麼一個 interface{}型別的 slice 是不是就可以接受任何型別的 slice ?
func printAll(vals []interface{}) { //1
for _, val := range vals {
fmt.Println(val)
}
}
func main(){
names := []string{"stanley", "david", "oscar"}
printAll(names)
}
複製程式碼
執行之後竟然會報 cannot use names (type []string) as type []interface {} in argument to printAll 錯誤,why?
這個錯誤說明 go 沒有幫助我們自動把 slice 轉換成 interface{} 型別的 slice,所以出錯了。go 不會對 型別是interface{} 的 slice 進行轉換 。
但是我們可以手動進行轉換來達到我們的目的。
var dataSlice []int = foo()
var interfaceSlice []interface{} = make([]interface{}, len(dataSlice))
for i, d := range dataSlice {
interfaceSlice[i] = d
}
複製程式碼
有個坑需要注意
如果是按 pointer 呼叫,go 會自動進行轉換,因為有了指標總是能得到指標指向的值是什麼,如果是 value 呼叫,go 將無從得知 value 的原始值是什麼,因為 value 是份拷貝。go 會把指標進行隱式轉換得到 value,但反過來則不行。
for
for 是 Go 中唯一的迴圈結構。這裡有 for 迴圈的三個基本使用方式。
最常用的方式,帶單個迴圈條件
i := 1
for i <= 3 {
fmt.Println(i)
i = i + 1
}
複製程式碼
經典的初始化/條件/後續形式 for 迴圈
for j := 7; j <= 9; j++ {
fmt.Println(j)
}
複製程式碼
不帶條件的 for 迴圈將一直執行,直到在迴圈體內使用了 break 或者 return 來跳出迴圈。
for {
fmt.Println("loop")
break
}
複製程式碼
if else
golang 中 if 要注意的點是
- 可省略條件表示式的括號。
- 支援初始化語句,可定義程式碼塊區域性變數。
- 程式碼塊左大括號必須在條件表示式尾部。
- 不支援三元操作符 “a > b ? a : b”
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 case default
switch sExpr {
case expr1:
some instructions
case expr2:
some other instructions
case expr3:
some other instructions
default:
other code
}
複製程式碼
sExpr和expr1、expr2、expr3的型別必須一致。Go的switch非常靈活,表示式不必是常量或整數,執行的過程從上至下,直到找到匹配項;而如果switch沒有表示式,它會匹配true。 Go裡面switch預設相當於每個case最後帶有break,匹配成功後不會自動向下執行其他case,而是跳出整個switch
fallthrough
在switch中,使用fallthrough可以強制執行後面的case程式碼。
switch sExpr {
case false:
fmt.Println("The integer was <= 4")
fallthrough
case true:
fmt.Println("The integer was <= 5")
fallthrough
case false:
fmt.Println("The integer was <= 6")
fallthrough
case true:
fmt.Println("The integer was <= 7")
case false:
fmt.Println("The integer was <= 8")
fallthrough
default:
fmt.Println("default case")
}
}
複製程式碼
輸出
The integer was <= 5
The integer was <= 6
The integer was <= 7
複製程式碼
for break continue goto range
for 是 Go 中唯一的迴圈結構。這裡有 for 迴圈的三個基本使用方式。
最常用的方式,帶單個迴圈條件。
i := 1
for i <= 3 {
fmt.Println(i)
i = i + 1
}
複製程式碼
經典的初始化/條件/後續形式 for 迴圈。
for j := 7; j <= 9; j++ {
fmt.Println(j)
}
複製程式碼
不帶條件的 for 迴圈將一直執行,直到在迴圈體內使用了 break 或者 return 來跳出迴圈。
for {
fmt.Println("loop")
break
}
複製程式碼
break是跳出本次迴圈,continue是跳過該次迴圈,繼續下次迴圈。
go
輕鬆開啟高併發,一直都是golang語言引以為豪的功能點。golang通過goroutine實現高併發,而開啟goroutine的鑰匙正是go關鍵字。開啟併發執行的語法格式是:
go funcName()
複製程式碼
select
Go的select關鍵字可以讓你同時等待多個通道操作,將協程(goroutine),通道(channel)和select結合起來構成了Go的一個強大特性。
package main
import "time"
import "fmt"
func main() {
// 本例中,我們從兩個通道中選擇
c1 := make(chan string)
c2 := make(chan string)
// 為了模擬並行協程的阻塞操作,我們讓每個通道在一段時間後再寫入一個值
go func() {
time.Sleep(time.Second * 1)
c1 <- "one"
}()
go func() {
time.Sleep(time.Second * 2)
c2 <- "two"
}()
// 我們使用select來等待這兩個通道的值,然後輸出
for i := 0; i < 2; i++ {
select {
case msg1 := <-c1:
fmt.Println("received", msg1)
case msg2 := <-c2:
fmt.Println("received", msg2)
}
}
}
複製程式碼
輸出結果
received one
received two
複製程式碼
如我們所期望的,程式輸出了正確的值。對於select語句而言,它不斷地檢測通道是否有值過來,一旦發現有值過來,立刻獲取輸出。
chan
channel[通道]是golang的一種重要特性,正是因為channel的存在才使得golang不同於其它語言。channel使得併發程式設計變得簡單容易有趣。
一個channel可以理解為一個先進先出的訊息佇列。如下圖所示:
建立channel有以下幾種 方式,
var ch chan string; // nil channel
ch := make(chan string); // zero channel
ch := make(chan string, 10); // buffered channel
複製程式碼
channel裡面的value buffer的容量也就是channel的容量。channel的容量為零表示這是一個阻塞型通道,非零表示緩衝型通道[非阻塞型通道]。
但是,這裡有個坑,當channel的容量為0時,for迴圈一次開10個goroutine列印輸出,此時理論上應該是順序輸出的,但是確實無序輸出的,這是因為現在的 Go 預設就是啟用的多核,不像以前版本還需要手動設定使用多核。
作者的 Golang 系列博文: