Go語言規格說明書 之 內建函數(Built-in functions)

go version go1.11 windows/amd64

 

本文爲閱讀Go語言中文官網的規則說明書(https://golang.google.cn/ref/spec)而做的筆記,介紹Go語言的 內建函數(Built-in functions)

 

規格說明書中的目錄如下:

Built-in functions
  -Close
  -Length and capacity
  -Allocation
  -Making slices, maps and channels
  -Appending to and copying slices
  -Deletion of map elements
  -Manipulating complex numbers
  -Handling panics
  -Bootstrapping

 

在規格說明書中,介紹了15個內建函數,如下所示:

close, len, cap, new, make, append, copy, delete,
complex, real, imag, panic, recover,
print, println

 

下面簡要介紹各個函數:

close

關閉信道(channel)。

在沒有更多數據 要 發送 的時候關閉信道。

close(channel)

也就是說,channel必須是 只發 或 雙向類型的纔可以,否則錯誤。

發送 或 關閉 一個已經關閉的 信道 會出現 運行時錯誤(run-time panic)。

關閉 nil 信道 也會導致 運行時錯誤。

在調用close函數,並且之前發送的數據都已被接收到 之後,接收操作 會 返回 信道類型的0值,且不會阻塞,,多個值的接收操作 會返回 一個接收值 和 信道是否關閉的標誌。

// 單個值接收操作
v1 := <-ch
v2 = <-ch

// 多(兩)個值接收操作
x, ok = <-ch
x, ok := <-ch
var x, ok = <-ch
var x, ok T = <-ch

 

len,cap

這兩個函數接收各種類型的參數,返回一個int型的結果。

len函數返回參數的 長度、元素個數,cap返回參數的 容量。

官文介紹:

Call      Argument type    Result

len(s)    string type      string length in bytes
          [n]T, *[n]T      array length (== n)
          []T              slice length
          map[K]T          map length (number of defined keys)
          chan T           number of elements queued in channel buffer

cap(s)    [n]T, *[n]T      array length (== n)
          []T              slice capacity
          chan T           channel buffer capacity

從官文介紹可以看出,len函數 的參數可以爲 字符串、數組類型、數組指針、分片、映射類型、信道——在信道緩存中排隊的元素數量,

                                    cap函數 的參數可以爲 數組、數組指針、分片、信道——緩存容量大小。

分片類型 的容量是指 分配給它的 底層數組 的元素的數量。在任何時候,下面的公式是成立的:

0 <= len(s) <= cap(s)

值爲 nil 的 分片、映射、信道 的長度爲0,值爲 nil 的分片的容量爲 0。

 

如果 s 是 字符串常量,那麼,表達式 len(s) 也是常量;

如果 s 是 數組 或者 指向數組的指針,那麼,表達式 len(s) 和 cap(s) 也是常量,並且 s 的元素類型不是 接收信道 或 函數類型,在這種情況下,s 是無法被估值的(evaluated);

除了上面的情況外,調用 len 和 cap 得到的都不是常量,而且s是可以估值的(翻譯的有些不準確,這個evaluated到底是什麼意思?)。

 

官文示例:

const (
	c1 = imag(2i)                    // imag(2i) = 2.0 is a constant
	c2 = len([10]float64{2})         // [10]float64{2} contains no function calls
	c3 = len([10]float64{c1})        // [10]float64{c1} contains no function calls
	c4 = len([10]float64{imag(2i)})  // imag(2i) is a constant and no function call is issued
	c5 = len([10]float64{imag(z)})   // invalid: imag(z) is a (non-constant) function call
)
var z complex128

 

new

new函數 在學習接口類型時遇到了,使用new,可以新建一個類型的實例。

new函數 屬於 Allocation(分配) 小節,分配 什麼呢?內存空間了。

官文翻譯:

在 運行時,給 變量 分配一個 存儲空間,這個空間大小由 變量類型決定,並且 返回 一個 變量類型的指針 指向分配的空間。

在分配完存儲空間後,再進行初始化——分配完畢後是0值。

 

疑問,不能在分配時進行初始化嗎?需要試驗!

type S1 struct {
	int
	float32
}

var sv2 = new(S1{23, 32.0}) // 錯誤:S1 literal is not a type
fmt.Println(sv2)
fmt.Println(sv2.int, sv2.float32)

看來不能在調用new函數時賦值!或者俺的方法不對!

更改代碼後測試:

var sv2 = new(S1)
sv2.int = 23
sv2.float32 = 32.0
fmt.Println(sv2)
fmt.Println(sv2.int, sv2.float32)

// 測試結果
// 注意第一行的 & 符號!
// 返回的 sv2 是一個 指針
&{23 32}
23 32

 

官文示例:

type S struct { a int; b float64 }
new(S)

 

make

make函數 屬於 Making slices, maps and channels 一節的唯一函數,用於常見 分片、映射和信道。

make函數 的第一個參數爲 類型T,可以選擇跟着一個表達式的指明類型的列表(原文:optionally followed by a type-specific list of expressions,翻譯存在問題)。

返回 類型T,而不是 *T,,空間初始化可以參考initial values

官文中介紹的用法:

Call             Type T     Result

make(T, n)       slice      slice of type T with length n and capacity n // 第二個參數n表示長度,沒有第三個參數時,n也表示容量
make(T, n, m)    slice      slice of type T with length n and capacity m // 第二個參數n表示長度,第三個參數m表示容量

make(T)          map        map of type T // 映射,但沒有分配初始空間
make(T, n)       map        map of type T with initial space for approximately n elements // 分配了初始空間的映射,可以容納n個元素,但可以自動擴展(需確認)

make(T)          channel    unbuffered channel of type T // 無緩衝信道
make(T, n)       channel    buffered channel of type T, buffer size n // 緩衝區大小爲 n(字節嗎?)的信道,關於信道的資料,自己還需dig

每一個n、m都必須是 整型(integer type,各種整型嗎?),或者一個 無類型的常量。一個常量大小參數不能是非負數,並且可以被類型int的值表示,,如果是無類型的常量,會被分配類型int。

n不能大於m。

如果n在運行時 爲 負數 或 大於m,會產生一個運行時錯誤。

 

官文示例——合法、非法使用的都有:

s := make([]int, 10, 100)       // slice with len(s) == 10, cap(s) == 100
s := make([]int, 1e3)           // slice with len(s) == cap(s) == 1000
s := make([]int, 1<<63)         // illegal: len(s) is not representable by a value of type int // 1<<63超過類型int的範圍了
s := make([]int, 10, 0)         // illegal: len(s) > cap(s)
c := make(chan int, 10)         // channel with a buffer size of 10
m := make(map[string]int, 100)  // map with initial space for approximately 100 elements

 

調用指定了類型和參數n的make函數創建映射時,會創建一個可以包含n個元素的初始化空間,但是,空間大小是和編譯器的實現相關的(implementation-dependent)——這個大小是無法統一。

 

append,copy

這兩個函數用來操作分片。從函數名可知,append用來給分片添加元素,copy用來從分片拷貝元素,拷貝到哪裏呢?拷貝到分片中。

對於這兩個函數,其返回結果是和 參數指向的內存是否覆蓋(whether the memory referenced by the arguments overlaps) 沒有關係的。

 

可變參函數append添加0到多個到分片,並返回 結果分片。

參數x中的值得類型T,必須是分片S的元素類型,但有一個特例,第一個參數爲 []byte類型,第二個參數爲string類型——但其結尾添加三個英文句號(...),這種情況表示將添加字符串的字節數組到第一個參數。

append(s S, x ...T) S  // T is the element type of S

如果分片s的容量不足於包含新添加的值,appen將分配新的、足夠大的 底層數組 來滿足 存在的分片和新增值 的空間,,否則,appen函數將使用 底層數組。

疑問,前面講到,append會返回新的分片,那麼,這個分片和舊的分片是什麼關係呢?是在舊的分片上操作的嗎?需要dig!之前自己操作時,是需要把append的返回值 賦值給 舊分片的變量的。

官文示例:

s0 := []int{0, 0}
s1 := append(s0, 2)                // append a single element     s1 == []int{0, 0, 2}
s2 := append(s1, 3, 5, 7)          // append multiple elements    s2 == []int{0, 0, 2, 3, 5, 7}
s3 := append(s2, s0...)            // append a slice              s3 == []int{0, 0, 2, 3, 5, 7, 0, 0}
s4 := append(s3[3:6], s3[2:]...)   // append overlapping slice    s4 == []int{3, 5, 7, 2, 3, 5, 7, 0, 0}

// 下面這個很有意思!用到了 空接口,分片可以包含 任何類型 的數據了 var t []interface{} t = append(t, 42, 3.1415, "foo") // t == []interface{}{42, 3.1415, "foo"} var b []byte b = append(b, "bar"...) // append string contents b == []byte{'b', 'a', 'r' }

 

copy函數從 源分片拷貝若干元素到目的分片,並返回拷貝的元素的數量。源分片和目的分片的元素類型T必須相同,並可以被賦值給[]T。

拷貝的元素的數量是 len(src)、len(dst) 中的最小值。

和append函數類似,copy函數也存在特例,目的分片爲 []byte類型,源分片爲字符串類型(string),這表示將字符串的字節數組拷貝到字符分片中。注意,特例不需要三個英文句號(...),這和append函數不同。

copy函數簽名:

copy(dst, src []T) int
copy(dst []byte, src string) int

copy函數官文示例:

var a = [...]int{0, 1, 2, 3, 4, 5, 6, 7}
var s = make([]int, 6)
var b = make([]byte, 5)
n1 := copy(s, a[0:])            // n1 == 6, s == []int{0, 1, 2, 3, 4, 5}
n2 := copy(s, s[2:])            // n2 == 4, s == []int{2, 3, 4, 5, 4, 5}
n3 := copy(b, "Hello, World!")  // n3 == 5, b == []byte("Hello")

 

delete

刪除一個映射 m 中的鍵值爲 k 的元素。k 的類型 必須是 可以賦值(assignable,這個不是很清楚什麼意思,類型相同 或 結果的類型相同) 給 映射 m 的鍵類型的。

delete(m, k)  // remove element m[k] from map m

如果映射 m 的值爲 nil,或者,m[k] 不存在,那麼,調用函數 delete 將什麼也不做(no-op)。

 

complex,real,imag

這三個函數屬於 操作複數(Manipulating complex numbers) 這一節。

大概意思:

complex函數 根據參數的 浮點類型的 實部、虛部 建立複數,real函數 獲取一個複數的 實部,imag函數 獲取一個複數的 虛部。

官文的三個函數簽名:

complex(realPart, imaginaryPart floatT) complexT
real(complexT) floatT
imag(complexT) floatT

 

complex的兩個參數必須是相同的浮點類型——float32、float64,float32生成複數類型爲complex64,float64生產的複數類型爲complex128。

如果一個參數是 沒有類型的常量(untyped constant),它的類型就被轉換爲另一個參數的類型;

如果兩個參數都是 沒有類型的常量,它們必須 不是複數、或者它們的虛部爲0,並且返回的是一個 沒有類型的複數(特別)。

 

對於real、imag函數,參數必須是複數類型,返回值的類型由參數的類型決定,對應關係:complex64-->float32,complex128-->float64。

如果參數時一個 沒有類型的常量,那麼,參數必須是數,並且返回值是 沒有類型的浮點常量(特別)。

 

real、imag函數 一起 形成了 complex函數 的反函數,對於一個複數類型Z的值z,下面的公式是成立的:

z == Z(complex(real(z), imag(z)))

 

如果這些函數的操作數都是 常量,那麼,返回值也是常量。

 

官文示例:

var a = complex(2, -2)             // complex128
const b = complex(1.0, -1.4)       // untyped complex constant 1 - 1.4i
x := float32(math.Cos(math.Pi/2))  // float32
var c64 = complex(5, -x)           // complex64
var s uint = complex(1, 0)         // untyped complex constant 1 + 0i can be converted to uint
_ = complex(1, 2<<s)               // illegal: 2 assumes floating-point type, cannot shift // 整數纔可以做 位運算
var rl = real(c64)                 // float32
var im = imag(a)                   // float64
const c = imag(b)                  // untyped constant -1.4
_ = imag(3 << s)                   // illegal: 3 assumes complex type, cannot shift // 整數纔可以做 位運算

 

panic,recover

這兩個函數分別用於 報告 和 處理 運行時錯誤 或 程序自定義的錯誤條件,簽名如下:

func panic(interface{})
func recover() interface{}

當然,panic函數 就是 報告一個錯誤,recover函數 就是負責處理的函數。

 

特別提醒,要讀懂這一節,需要理解 Go語言中 的 Defer statements(延遲語句?)

 

下面是官文翻譯:

在執行 函數F 的時候,顯式調用panic函數或者發生運行時錯誤(run-time panic)都會 終斷函數F的執行,但是,任何被 函數F 延遲的函數 會立即被執行。

也就是說,函數F中發生錯誤了,如果沒有 延遲函數,那麼,函數F 被終斷,如果有延遲函數,則執行完延遲函數後再終斷。

前面講到 函數F 的延遲函數被調用,所有的延遲函數執行完畢後,,接下來,函數F 的調用者 的延遲函數 開始執行,,直到goroutine的頂級函數的延遲函數被執行。

看到了吧,發生了錯誤,就一直是 延遲函數 在執行,直到goroutine的頂級函數的延遲函數被執行。那麼,什麼事goroutine呢?goroutine的頂級函數的延遲函數被執行 之後怎麼樣呢?銷燬goroutine?

在這個時刻,程序被終止,錯誤條件被報告,同時被報告的還有函數panic的參數的值。這個終止序列被稱爲 panicking(翻譯爲什麼好呢?驚險追蹤?錯誤追蹤?)

官文示例——調用panic函數:

panic(42)
panic("unreachable")
panic(Error("cannot parse"))

上面講了panic函數,用於 顯示製造錯誤,在錯誤發生後,延遲函數開始執行,直到goroutine的頂級函數的延遲函數被執行完畢。

注意,延遲函數 在沒有發生錯誤時也會執行,時機爲 函數返回前(有返回return語句的) 和 函數結束前。

 

從上面也看到了,如果不阻止panic的擴散,程序(goroutine?這個自己還不完全清楚)會被終止!那麼,怎麼阻止呢?這就需要 recover函數 了!

 

recover函數 使用一段程序 去管理一個panicking goroutine的行爲。

設想一下,函數G 延遲了 函數D 的執行,函數D中調用了 recover函數。

在同一個goroutine中,函數G發生了panic(錯誤),此時,被函數G延遲的函數開始執行——其中包括函數D,當執行到函數D時,函數D 中調用recover函數 的返回值 是傳遞到 調用panic函數中的值。

如果 函數D 正常返回,也沒有產生新的panic,panicking這個過程就結束了

在這種情況下,調用函數G到調用函數panic(發生錯誤)期間的函數狀態就被忽略了,並且繼續程序的正常執行。

前面到了函數D被執行了, 返回了,panicking結束了,此時,在函數D之前被函數G 延遲的 函數開始執行——如果有,函數G的執行被終止,返回它的調用者。

以上,便是執行recover函數的“內幕”!調用recover且沒有產生新的panic就表示錯誤被處理了。程序還是在函數G中執行,但執行的是函數G的延遲函數,執行完延遲函數了,就返回到它的調用者。

 

前面提到 recover函數 有返回值,值爲panic函數的參數。那麼,recover函數調用後什麼時候其返回值爲 nil 呢?下面幾種情況之一滿足即返回 nil:

-panic函數的參數爲 nil (空嗎?)

-goroutine沒有發生panicking——就是沒錯的時候調用recover恢復;

-recover函數不是被延遲函數直接調用的——這是什麼情況?還需dig!

 

官文示例:protect函數 調用了 參數中的 函數g,並延遲了一個函數 用於處理 調用函數g 引發的panic(run-time panics)。

func protect(g func()) {
	defer func() { // 延遲函數定義,關鍵字 defer
		log.Println("done")  // Println executes normally even if there is a panic
		if x := recover(); x != nil { // 調用recover:如果發生錯誤,返回值 x 就不是 nil,此時,處理錯誤,只是打印一條信息
			log.Printf("run time panic: %v", x)
		}
	}()
	log.Println("start")
	g()
}

 

幾乎理解了,還需要熟悉 goroutine,以及閱讀和練習更多Go代碼才行啊!

 

print,println

目前的Go語言實現提供了一些在引導時有用的內建函數,文檔爲了完整性會介紹這些函數,但是,不保證它們會存在於Go語言中,而且它們不會返回結果。

比如,本節的print、println兩個函數,都用來輸出 參數。

官文函數說明:

Function   Behavior

print      prints all arguments; formatting of arguments is implementation-specific
println    like print but prints spaces between arguments and a newline at the end

實現(所謂的實現,是指,各個軟件開發組織 根據 這份規格說明書 來開發Go語言編譯器的實現吧)要求:

print、pringln不需要接受所有的參數類型,但是,打印 布爾、數值、字符串類型 是必須支持的。

 

後記

又是小半個下午加晚上的一兩個小時,這篇博文可是花了5個小時左右啊!

缺少一些示例,自己的實踐,不夠完美,但是呢,大部分知識點是講清楚了,不清楚的也是有標記的。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章