剖析 golang 的25個關鍵字

chengkai發表於2019-03-04

剖析 golang 的25個關鍵字

基本在所有語言當中,關鍵字都是不允許用於自定義的,在Golang中有25個關鍵字,圖示如下:

d

下面我們逐個解析這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可以理解為一個先進先出的訊息佇列。如下圖所示:

d

建立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 系列博文:

客觀對比Node 與 Golang

Array、Slice、Map原理淺析

Go 與 Node 記憶體分配與垃圾回收

goroutine 排程原理