1. 程式人生 > >Golang快速入門學習筆記

Golang快速入門學習筆記

Golang快速入門學習筆記

安裝

# 我是使用的linux mint,直接通過包管理工具安裝
sudo apt-get install golang
# 安裝之後直接檢視GOPATH的路徑,這個是最主要的
go env
# 如果是空,直接編輯/etc/profile,跟上自己需要的目錄
export GOPATH="/home/jcleng/go"
# 其次為了go編譯的二進位制檔案能直接到終端執行,也可以直接
export PATH=$PATH:/home/jcleng/go/bin

hello world

# 一個專案只有一個package為main的包,func為mian的方法,可以理解為程式的入口檔案
# import關鍵字匯入包fmt,fmt包是內建,Println方法使用.連線呼叫
package main
import "fmt"
func main() {
    fmt.Println("Hello World")
}

變數&&常量

# 變數使用 var 關鍵字
var age int // 變數宣告
age = 29 // 賦值

var age int = 29 // 宣告變數並初始化
# 簡短宣告使用了 := 操作符
# 注意,簡短宣告要求:
	:= 操作符左邊的所有變數都有初始值
	:= 操作符的左邊至少有一個變數是尚未宣告的
name, age := "naveen", 29 // 簡短宣告
num := int(5) // 增加型別

# 常量使用const,常量的值會在編譯的時候確定。因為函式呼叫發生在執行時,所以不能將函式的返回值賦值給常量。

# type關鍵字型別宣告
var defaultName = "Sam" // 允許
type myString string
var customName myString = "Sam" // 允許
customName = defaultName // 不允許

型別

bool
數字型別
		int8, int16, int32, int64, int // 注意:根據不同的底層平臺(Underlying Platform),表示 32 或 64 位整型。除非對整型的大小有特定的需求,否則你通常應該使用 int 表示整型。
		uint8, uint16, uint32, uint64, uint
		float32, float64
		complex64, complex128
		byte // byte 是 uint8 的別名。
		rune // rune 是 int32 的別名。
string
# 測試輸出
math.MaxInt32 // 大小
fmt.Printf("type of a is %T, size of a is %d", a, unsafe.Sizeof(a)) // a 的型別和大小
# 關於型別轉換,只有顯示轉換
sum := i + int(j)

函式

# 關鍵詞 func 開始,後面緊跟自定義的函式名 functionname (函式名)。函式的引數列表定義在 ( 和 ) 之間,返回值的型別則定義在之後的 returntype (返回值型別)處。宣告一個引數的語法採用 引數名 引數型別 的方式,任意多個引數採用類似 (parameter1 type, parameter2 type) 即(引數1 引數1的型別,引數2 引數2的型別)的形式指定。之後包含在 { 和 } 之間的程式碼,就是函式體。
func functionname(parametername type) (returntype,returntype) {  
    // 函式體(具體實現的功能)
}
# 注意:多返回值
# 返回值必須用括號括起來:(float64, float64)
func rectProps(length, width float64)(float64, float64) {  
    var area = length * width
    var perimeter = (length + width) * 2
    return area, perimeter
}
func main() {  
    area, perimeter := rectProps(10.8, 5.6)
    fmt.Printf("Area %f Perimeter %f", area, perimeter) 
}
# 關於:命名返回值(area, perimeter float64),直接return
func rectProps(length, width float64)(area, perimeter float64) {  
    area = length * width
    perimeter = (length + width) * 2
    return // 不需要明確指定返回值,預設返回 area, perimeter 的值
}
# 機智的:空白符
# _ 在 Go 中被用作空白符,可以用作表示任何型別的任何值。
# 先後順序還是要注意的,根據return area, perimeter先後順序
func rectProps(length, width float64) (float64, float64) {  
    var area = length * width
    var perimeter = (length + width) * 2
    return area, perimeter
}
func main() {  
    area, _ := rectProps(10.8, 5.6) // 返回值周長被丟棄
    fmt.Printf("Area %f ", area)
}

包 package

# 所有可執行的 Go 程式都必須包含一個 main 函式。這個函式是程式執行的入口。main 函式應該放置於 main 包中,之前已經提過。
# 自定義包名稱一般是父級目錄的名稱
# 路徑
src
    geometry
        geometry.go
        rectangle
            rectprops.go
# geometry.go是main包,那麼rectprops.go的包就是rectangle
package rectangle
# 關於外部呼叫:如 geometry.go 呼叫 rectprops.go 
import (  
    "fmt"
    "geometry/rectangle" // 匯入自定義包,這裡是相對src資源目錄
)
# 只有rectprops.go檔案裡面的函式首字母大寫才是自動匯出的方法才能夠使用,可以理解為大寫就是public,小寫就是private
func Diagonal(len, wid float64) float64 {  
    diagonal := math.Sqrt((len * len) + (wid * wid))
    return diagonal
}
# 呼叫
rectprops.Diagonal(2.0,2.0)

# 關於初始函式,在所有的函式之前執行,所有包都可以包含一個 init 函式。init 函式不應該有任何返回值型別和引數,在我們的程式碼中也不能顯式地呼叫它。
func init() {  
}

# 包 使用空白識別符號
# 全域性使用
var _ = rectangle.Area // 錯誤遮蔽器,不呼叫也不會提示編譯錯誤
# 匯入包的時候使用 
import (  
    _ "geometry/rectangle" // 初始化包,但是不使用也不會提示編譯錯誤
)

語句

# if
# 注意,條件沒有括號,else 語句應該在 if 語句的大括號 } 之後的同一行中。如果不是,編譯器會不通過,golang是編譯器自動增加分號的
if condition {  
} else if condition {
} else {
}
# 特殊,條件可以先做運算,num作用域僅限if結構塊裡面
if num := 10; num % 2 == 0 { //checks if number is even

# for
# for 是 Go 語言唯一的迴圈語句。Go 語言中並沒有其他語言比如 C 語言中的 while 和 do while 迴圈。
for initialisation; condition; post {  
}
# 也可以先運算
func main() {  
    for i := 1; i <= 10; i++ {
        if i > 5 {
            break //loop is terminated if i > 5
        }
        fmt.Printf("%d ", i)
    }
    fmt.Printf("\nline after for loop")
}

# switch
# case自帶break,使用fallthrough不跳出
func main() {
    finger := 4
    switch finger {
    case 1:
        fmt.Println("Thumb")
    case 2:
        fmt.Println("Index")
    case 3:
        fmt.Println("Middle")
    case 4:
        fmt.Println("Ring")
    default:
        fmt.Println("Pinky")
    }
}

Arrays 陣列 和 Slices 切片

# 陣列的表示形式為 [n]Type,一個數組的Type全部一樣
# 注意:陣列的大小是型別的一部分。因此 [5]int 和 [25]int 是不同型別。陣列不能調整大小,不要擔心這個限制,因為 slices 的存在能解決這個問題。陣列是值型別。
var a [3]int //int array with length 3
b := [3]int{12, 78, 50} // short hand declaration to create array
fmt.Println(a)
fmt.Println(b)

a := [...]string{"USA", "China", "India", "Germany", "France"}
a[0] = "Singapore"
# 關於迭代陣列
a := [...]float64{67.7, 89.8, 21, 78}
for i := 0; i < len(a); i++ { // looping from 0 to the length of the array
    fmt.Printf("%d th element of a is %.2f\n", i, a[i])
}

for i, v := range a {//range returns both the index and value
    fmt.Printf("%d the element of a is %.2f\n", i, v)
    fmt.Printf("%d th element of a is %.2f\n", i, a[i])
}
# 如果你只需要值並希望忽略索引,則可以通過用 _ 空白識別符號替換索引來執行。
for _, v := range a { // ignores index  
}
# 多維陣列一樣

# Slices 切片
# 切片是由陣列建立的一種方便、靈活且功能強大的包裝(Wrapper)。切片本身不擁有任何資料。它們只是對現有陣列的引用。
var names []string // 值為nil的切片,“空切片”
# (names == nil) == ture

countriesCpy := make([]string, 10)
fmt.Println(countriesCpy) // 值不為nil的切片,“空切片”,這裡的長度10,可以在copy切片的時候獲取需要copy切片的長度
# (countriesCpy == nil) == false

a := [5]int{76, 77, 78, 79, 80}
var b []int = a[1:4] // a[start:end] 建立一個從 a 陣列索引 start 開始到 end - 1 結束的切片 end>len 會編譯錯誤
b[0] = 0
fmt.Println(a)
fmt.Println(b)
# [76 0 78 79 80]
# [0 78 79]

# {76, 77, 78, 79, 80}
#  0   1   2    3  4

# 另一種
c := []int{6, 7, 8} // 建立一個有 3 個整型元素的陣列,並返回一個儲存在 c 中的切片引用。
# 另一種
numa := [3]int{78, 79, 80}
nums1 := numa[:] // 建立一個一樣的切片

# 切片自己不擁有任何資料。它只是底層陣列的一種表示。對切片所做的任何修改都會反映在底層陣列中。當多個切片共用相同的底層陣列時,每個切片所做的更改將反映在陣列中。
# 如上
# b[0] = 0
# [76 0 78 79 80]
# [0 78 79]

# 切片的長度和容量
fmt.Println("After re-slicing length is",len(fruitslice), "and capacity is",cap(fruitslice))

# 使用 make 建立一個切片,func make([]T,len,cap)[]T 通過傳遞型別,長度和容量來建立切片。容量是可選引數, 預設值為切片長度,並返回引用該陣列的切片
i := make([]int, 5, 5)
fmt.Println(i)

# 追加切片元素
# 說明:如果切片由陣列支援,並且陣列本身的長度是固定的,那麼切片如何具有動態長度。以及內部發生了什麼,當新的元素被新增到切片時,會建立一個新的陣列。現有陣列的元素被複制到這個新陣列中,並返回這個新陣列的新切片引用。現在新切片的容量是舊切片的兩倍。
# 正如我們已經知道陣列的長度是固定的,它的長度不能增加。 切片是動態的,使用 append 可以將新元素追加到切片上。append 函式的定義是 func append(s[]T,x ... T)[]T
cars := []string{"Ferrari", "Honda", "Ford"}
fmt.Println("cars:", cars, "has old length", len(cars), "and capacity", cap(cars))
cars = append(cars, "Toyota")
fmt.Println("cars:", cars, "has new length", len(cars), "and capacity", cap(cars))
# 輸出
# cars: [Ferrari Honda Ford] has old length 3 and capacity 3
# cars: [Ferrari Honda Ford Toyota] has new length 4 and capacity 6

# ... 運算子  將一個切片新增到另一個切片(可變引數函式教程中瞭解有關此運算子的更多資訊:printf("%d", i);)
veggies := []string{"potatoes", "tomatoes", "brinjal"}
fruits := []string{"oranges", "apples"}
food := append(veggies, fruits...)
fmt.Println("food:", food)
# food: [potatoes tomatoes brinjal oranges apples]

# 切片的函式傳遞
func subtactOne(numbers []int) {
	for i := range numbers {
		numbers[i] -= 2
	}
}
func main() {
	nos := []int{8, 7, 6}
	fmt.Println("slice before function call", nos)
	subtactOne(nos)                               // function modifies the slice 傳遞切片,類似引用傳遞,不能直接使用陣列傳遞
	fmt.Println("slice after function call", nos) // modifications are visible outside
}

# 多維切片一樣的

# 記憶體優化
# 切片持有對底層陣列的引用。只要切片在記憶體中,陣列就不能被垃圾回收。在記憶體管理方面,這是需要注意的。讓我們假設我們有一個非常大的陣列,我們只想處理它的一小部分。然後,我們由這個陣列建立一個切片,並開始處理切片。這裡需要重點注意的是,在切片引用時陣列仍然存在記憶體中。
# 一種解決方法是使用 copy 函式 func copy(dst,src[]T)int 來生成一個切片的副本。這樣我們可以使用新的切片,原始陣列可以被垃圾回收。
countries := []string{"USA", "Singapore", "Germany", "India", "Australia"}
neededCountries := countries[:len(countries)-2]
countriesCpy := make([]string, len(neededCountries)) // 長度為len(neededCountries)的切片,空的,但是並不是nil
copy(countriesCpy, neededCountries) //copies neededCountries to countriesCpy 現在 countries 陣列可以被垃圾回收, 因為 neededCountries 不再被引用,已經copy到countriesCpy
return countriesCpy

可變引數函式

# append 函式是如何將任意個引數值加入到切片中的。這樣 append 函式可以接受不同數量的引數。
# 可變引數函式的工作原理是把可變引數轉換為一個新的切片。
# 我們沒有給可變引數 nums ...int 傳入任何引數。這也是合法的,在這種情況下 nums 是一個長度和容量為 0 的 nil 切片。
func find(num int, nums ...int) { // 最後一個引數...,接受所有的值,num = 89 ,nums = [89 90 95]
	fmt.Printf("type of nums is %T\n", nums)
	found := false
	for i, v := range nums {
		if v == num {
			fmt.Println(num, "found at index", i, "in", nums)
			found = true
		}
	}
	if !found {
		fmt.Println(num, "not found in ", nums)
	}
	fmt.Printf("\n")
}
func main() {
	find(89, 89, 90, 95)
}

# 給可變引數函式傳入切片
# 有一個可以直接將切片傳入可變引數函式的語法糖,你可以在在切片後加上 ... 字尾。如果這樣做,切片將直接傳入函式,不再建立新的切片
func find(num int, nums ...int) {
    fmt.Printf("type of nums is %T\n", nums)
    found := false
    for i, v := range nums {
        if v == num {
            fmt.Println(num, "found at index", i, "in", nums)
            found = true
        }
    }
    if !found {
        fmt.Println(num, "not found in ", nums)
    }
    fmt.Printf("\n")
}
func main() {
    nums := []int{89, 90, 95} // 長度3 容量3 的切片
    find(89, nums...) // ...在後不再轉換傳入切片,因為可變引數函式接受的...就是一個切片,不加...就相當於:find(89, []int{nums}),編譯器會報錯
}
# 2種方式
func modify(arr ...int) { // 接受並建立一個切片
	fmt.Println(arr)
}
func main() {
	a := [3]int{89, 90, 91} // 陣列
	b := a[:]               // 切片
	modify(b...)            // 傳入非切片,不再建立新的切片
	modify2(b)              // 傳入一個切片
}
func modify2(arr []int) { // 接受切片
	fmt.Println(arr)
}
# 測試
func change(s ...string) {
	s[0] = "Go"
}
func main() {
	welcome := []string{"hello", "world"}
	change(welcome...)
	fmt.Println(welcome)
}
# 輸出 [Go world]
func change(s ...string) {
	s[0] = "Go"
	s = append(s, "playground")
	fmt.Println(s)
}
func main() {
	welcome := []string{"hello", "world"}
	change(welcome...)
	fmt.Println(welcome)
}
# 輸出[Go world playground][Go world]

Maps

# map 是在 Go 中將值(value)與鍵(key)關聯的內建型別。通過相應的鍵可以獲取到值。
# 建立,通過向 make 函式傳入鍵和值的型別,可以建立 map。make(map[type of key]type of value) 是建立 map 的語法。
personSalary := make(map[string]int) // 不為nil的空map
var personSalary map[string]int // 為nil的空map,map 的零值是 nil,建立map之後,必須make初始化才能賦值
personSalary = make(map[string]int)
# make之後才能賦值
personSalary["steve"] = 12000
# 
personSalary := map[string]int {
    "steve": 12000,
    "jamie": 15000,
}
personSalary["mike"] = 9000
fmt.Println("personSalary map contents:", personSalary)
fmt.Println(personSalary["mike"]) // 獲取

# map 中到底是不是存在這個 key,如果 ok 是 true,表示 key 存在,key 對應的值就是 value ,反之表示 key 不存在。
value, ok := personSalary["mike"]
if ok {
	fmt.Println(value)
}
# 遍歷
for key, value := range personSalary {
    fmt.Printf("personSalary[%s] = %d\n", key, value)
}
# 操作元素
# 刪除 map 中 key 的語法是 delete(map, key)。這個函式沒有返回值。
# map的長度len(map)

# 注意:Map 是引用型別,和 slices 類似,當 map 被賦值為一個新變數的時候,它們指向同一個內部資料結構。因此,改變其中一個變數,就會影響到另一變數。
personSalary := map[string]int{
	"steve": 12000,
	"jamie": 15000,
}
personSalary["mike"] = 9000
fmt.Println("Original person salary", personSalary)
newPersonSalary := personSalary
newPersonSalary["mike"] = 18000
fmt.Println("Person salary changed", personSalary)
# 輸出
# Original person salary map[steve:12000 jamie:15000 mike:9000]
# Person salary changed map[steve:12000 jamie:15000 mike:18000]

# Map 的相等性
# map 之間不能使用 == 操作符判斷,== 只能用來檢查 map 是否為 nil,如需對比,請自行便遍歷對比

字串

# 字串是一個位元組切片,列印每一個字元
func printBytes(s string) {
    for i:= 0; i < len(s); i++ {
        fmt.Printf("%x ", s[i]) // %x 格式限定符用於指定 16 進位制編碼。%c 格式限定符用於列印字串的字元。
    }
}
func main() {
    name := "Hello World"
    printBytes(name) // 48 65 6c 6c 6f 20 57 6f 72 6c 64
}
# rune
# rune 是 Go 語言的內建型別,它也是 int32 的別稱。在 Go 語言中,rune 表示一個程式碼點。程式碼點無論佔用多少個位元組,都可以用一個 rune 來表示。讓我們修改一下上面的程式,用 rune 來列印字元。
func printChars(s string) {
	runes := []rune(s)
	for i := 0; i < len(runes); i++ {
		fmt.Printf("%c ", runes[i])
	}
}
func main() {
	name := "Señor" // ñ 佔了兩個位元組,需要使用rune
	printChars(name)
	// 遍歷
	for index, rune := range name {
		fmt.Printf("%c starts at byte %d\n", rune, index)
	}
}

# 用位元組切片構造字串
byteSlice := []byte{0x43, 0x61, 0x66, 0xC3, 0xA9}
str := string(byteSlice)
fmt.Println(str) // Café,上面的程式中 byteSlice 包含字串 Café 用 UTF-8 編碼後的 16 進位制位元組。程式輸出結果是 Café

byteSlice := []byte{67, 97, 102, 195, 169}// 把 16 進位制換成對應的 10 進位制
str := string(byteSlice)
fmt.Println(str)
# 字串的長度
# utf8 package 包中的 func RuneCountInString(s string) (n int) 方法用來獲取字串的長度。這個方法傳入一個字串引數然後返回字串中的 rune 的數量。
import (  
    "fmt"
    "unicode/utf8"
)
# 注意:字串是不可變的,Go 中的字串是不可變的。一旦一個字串被建立,那麼它將無法被修改。
func mutate(s string)string {  
    s[0] = 'a'//any valid unicode character within single quote is a rune  編譯器會報錯 cannot assign to s[0]
    return s
}
func main() {  
    h := "hello"
    fmt.Println(mutate(h))
}
# 為了修改字串,可以把字串轉化為一個 rune 切片。然後這個切片可以進行任何想要的改變,然後再轉化為一個字串。
func mutate(s []rune) string {  // 接收一個 rune 切片引數
    s[0] = 'a' 
    return string(s)
}
func main() {  
    h := "hello"
    fmt.Println(mutate([]rune(h)))
}

指標

# 指標變數的型別為 *T,該指標指向一個 T 型別的變數,指標的零值是 nil。Go 不支援指標運算。
b := 255
var a *int = &b // 宣告a是一個指標型別,使用&獲取b的記憶體地址
fmt.Printf("Type of a is %T\n", a) // Type of a is *int
fmt.Println("address of b is", a) // address of b is 0xc4200160f8

# 指標的解引用
b := 255
a := &b
fmt.Println("address of b is", a) // address of b is 0xc4200160f8
fmt.Println("value of b is", *a) // value of b is 255

# 修改值
b := 255
a := &b
fmt.Println("address of b is", a) // address of b is 0xc4200160f8
fmt.Println("value of b is", *a) // value of b is 255
*a++
fmt.Println("new value of b is", b) // new value of b is 256

# 向函式傳遞指標引數
func change(val *int) {  // 接受指標型別
    *val = 55
}
func main() {  
    a := 58
    fmt.Println("value of a before function call is",a)
    b := &a
    change(b)
    fmt.Println("value of a after function call is", a)
}

# 注意:不要向函式傳遞陣列的指標,而應該使用切片
# 假如我們想要在函式內修改一個數組,並希望呼叫函式的地方也能得到修改後的陣列,一種解決方案是把一個指向陣列的指標傳遞給這個函式。
func modify(arr *[3]int) {  
    (*arr)[0] = 90 // 把 arr 解引用,將 90 賦值給這個陣列的第一個元素,簡寫:arr[0] = 90
}
func main() {  
    a := [3]int{89, 90, 91}
    modify(&a) // 將陣列的地址傳遞給了 modify 函式。 使用切片modify(a[:]),接受切片modify(arr []int)
    fmt.Println(a) // [90 90 91]
}

結構體

# 結構體是使用者定義的型別,表示若干個欄位(Field)的集合。有時應該把資料整合在一起,而不是讓這些資料沒有聯絡。這種情況下可以使用結構體。
# 先宣告
type Employee struct {
    firstName string
    lastName  string
    age       int
}
# 賦值
emp1 := Employee{
    firstName: "Sam",
    age:       25,
    lastName:  "Anderson",
}
# 建立匿名結構體,不宣告
emp3 := struct {
	firstName, lastName string
	age, salary         int
}{
	firstName: "Andreah",
	lastName:  "Nikola",
	age:       31,
	salary:    5000,
}
fmt.Println("Employee 3", emp3)
# 結構體的零值(Zero Value)使用型別的預設值
var emp4 Employee //zero valued structure
fmt.Println("Employee 4", emp4) // Employee 4 {  0}
# 訪問結構體的欄位
emp6 := Employee{"Sam", "Anderson", 55}
fmt.Println("First Name:", emp6.firstName)
# 建立零值的 struct,以後再給各個欄位賦值。
var emp7 Employee
emp7.firstName = "Jack"
emp7.lastName = "Adams"
fmt.Println("Employee 7:", emp7)

# 結構體的指標
func main() {  
    emp8 := &Employee{"Sam", "Anderson", 55, 6000}
    fmt.Println("First Name:", (*emp8).firstName) // First Name: Sam go可以省略(*emp8)為emp8
    fmt.Println("Age:", emp8.age) // Age: 55
    fmt.Println("Age:", (*emp8).age) // Age: 55
}

# 匿名欄位
type Person struct {  
    string
    int
}
func main() {  
    p := Person{"Naveen", 50}
    fmt.Println(p.string)
}

# 巢狀結構體(Nested Structs)一樣的,當欄位唯一時,可以提升欄位
func main() {  
    var p Person
    p.name = "Naveen"
    p.age = 50
    p.Address = Address{
        city:  "Chicago",
        state: "Illinois",
    }
    fmt.Println("Name:", p.name)
    fmt.Println("Age:", p.age)
    fmt.Println("City:", p.city) //city is promoted field 提升欄位
    fmt.Println("State:", p.state) //state is promoted field 提升欄位
}
# 匯出結構體和欄位
# 如果結構體名稱以大寫字母開頭,則它是其他包可以訪問的匯出型別(Exported Type)。同樣,如果結構體裡的欄位首字母大寫,它也能被其他包訪問到。
var spec computer.Spec // computer包裡面的結構體Spec
spec.Maker = "apple"
spec.Price = 50000
fmt.Println("Spec:", spec)

# 結構體相等性(Structs Equality)
# 結構體是值型別。如果它的每一個欄位都是可比較的,則該結構體也是可比較的。如果兩個結構體變數的對應欄位相等,則這兩個變數也是相等的。如果結構體包含不可比較的欄位,則結構體變數也不可比較。比如map

方法

# 方法其實就是一個函式,在 func 這個關鍵字和方法名中間加入了一個特殊的 接收器 型別。接收器可以是結構體型別或者是非結構體型別。接收器是可以在方法的內部訪問的。
# 建立了一個接收器型別為 Type 的方法 methodName
func (t Type) methodName(parameter list) {
}
# 例子
package main

import (
    "fmt"
)

type Employee struct {
    name     string
    salary   int
    currency string
}

/*
  displaySalary() 方法將 Employee 做為接收器型別
*/
func (e Employee) displaySalary() {
    fmt.Printf("Salary of %s is %s%d", e.name, e.currency, e.salary)
}

func main() {
    emp1 := Employee {
        name:     "Sam Adolf",
        salary:   5000,
        currency: "$",
    }
    emp1.displaySalary() // 呼叫 Employee 型別的 displaySalary() 方法
}
# 使用函式寫出相同的程式,為什麼我們需要方法
# Go 不是純粹的面向物件程式語言,而且Go不支援類。因此,基於型別的方法是一種實現和類相似行為的途徑。
# 例子
package main

import (
    "fmt"
    "math"
)

type Rectangle struct {
    length int
    width  int
}

type Circle struct {
    radius float64
}

func (r Rectangle) Area() int {
    return r.length * r.width
}

func (c Circle) Area() float64 {
    return math.Pi * c.radius * c.radius
}

func main() {
    r := Rectangle{
        length: 10,
        width:  5,
    }
    fmt.Printf("Area of rectangle %d\n", r.Area())
    c := Circle{
        radius: 12,
    }
    fmt.Printf("Area of circle %f", c.Area())
}

# 指標接收器與值接收器
# 還可以建立使用指標接收器的方法。值接收器和指標接收器之間的區別在於, 在指標接收器的方法內部的改變對於呼叫者是可見的 ,然而值接收器的情況不是這樣的。
package main

import (
	"fmt"
)

type Employee struct {
	name string
	age  int
}

/*
使用值接收器的方法。
*/
func (e Employee) changeName(newName string) {
	e.name = newName
}

/*
使用指標接收器的方法。
*/
func (e *Employee) changeAge(newAge int) {
	e.age = newAge
}

func main() {
	e := Employee{
		name: "Mark Andrew",
		age:  50,
	}
	e.changeAge(50)
	fmt.Println(e) // {Mark Andrew 50} 所做的改變對呼叫者是不可見的,沒有改變值
	(&e).changeAge(100) {Mark Andrew 100} 所做的改變對呼叫者是不可見的,改變值  如果函式不重名,那麼可以直接e.changeAge(100),省略&
	fmt.Println(e)
}
# 那麼什麼時候使用指標接收器,什麼時候使用值接收器
# 一般來說,指標接收器可以使用在:對方法內部的接收器所做的改變應該對呼叫者可見時。
# 指標接收器也可以被使用在如下場景:當拷貝一個結構體的代價過於昂貴時。考慮下一個結構體有很多的欄位。在方法內使用這個結構體做為值接收器需要拷貝整個結構體,這是很昂貴的。在這種情況下使用指標接收器,結構體不會被拷貝,只會傳遞一個指標到方法內部使用。
# 在其他的所有情況,值接收器都可以被使用。

# 匿名欄位的方法
# 屬於結構體的匿名欄位的方法可以被直接呼叫,就好像這些方法是屬於定義了匿名欄位的結構體一樣。
# 即在多重結構體裡面,使用 p.fullAddress() 來訪問 address 結構體的 fullAddress() 方法。明確的呼叫 p.address.fullAddress() 是沒有必要的。

# 在方法中使用值接收器 與 在函式中使用值引數
# 當一個函式有一個值引數,它只能接受一個值引數。
# 當一個方法有一個值接收器,它可以接受值接收器和指標接收器。
# 例子
package main

import (
	"fmt"
)

type rectangle struct {
	length int
	width  int
}

func area(r rectangle) {
	fmt.Printf("Area Function result: %d\n", (r.length * r.width))
}

func (r rectangle) area() {
	fmt.Printf("Area Method result: %d\n", (r.length * r.width))
}

func main() {
	r := rectangle{
		length: 10,
		width:  5,
	}
	area(r) // 呼叫Function
	r.area()

	p := &r
	//area(p) // 這裡p是指標,編譯會報錯,Function不接受指標

	p.area() // 通過指標呼叫值接收器,那麼就是呼叫Method,接受指標
}
# Area Function result: 50
# Area Method result: 50
# Area Method result: 50

# 在方法中使用指標接收器 與 在函式中使用指標引數
# 和值引數相類似,函式使用指標引數只接受指標,而使用指標接收器的方法可以使用值接收器和指標接收器。
# 例子
package main

import (
    "fmt"
)

type rectangle struct {
    length int
    width  int
}

func perimeter(r *rectangle) {
    fmt.Println("perimeter function output:", 2*(r.length+r.width))

}

func (r *rectangle) perimeter() {
    fmt.Println("perimeter method output:", 2*(r.length+r.width))
}

func main() {
    r := rectangle{
        length: 10,
        width:  5,
    }
    p := &r
    perimeter(p) // 傳遞指標,function (r *rectangle)接受指標
    p.perimeter() // 傳遞指標,method本來就接收指標

    //perimeter(r) // 這裡不是指標,編譯器報錯cannot use r (type rectangle) as type *rectangle in argument to perimeter

    r.perimeter()// 使用值來呼叫指標接收器,可以使用值接收器和指標接收器
    (&r).perimeter() // 與上面一樣
}
# perimeter function output: 30
# perimeter method output: 30
# perimeter method output: 30

# 在非結構體上的方法
# 為了在一個型別上定義一個方法,方法的接收器型別定義和方法的定義應該在同一個包中。到目前為止,我們定義的所有結構體和結構體上的方法都是在同一個 main 包中,因此它們是可以執行的。
# 例子
package main

import "fmt"

type myInt int

func (a myInt) add(b myInt) myInt {
	return a + b
}

func main() {
	// num1 := myInt(5) // 一樣的
	var num1 myInt = 5
	num2 := myInt(10)
	sum := num1.add(num2)
	fmt.Println("Sum is", sum)
}

介面(一)
Go 系列教程 —— 18. 介面(一)

# 在面向物件的領域裡,介面一般這樣定義:介面定義一個物件的行為。介面只指定了物件應該做什麼,至於如何實現這個行為(即實現細節),則由物件本身去確定。
type SalaryCalculator interface {
	CalculateSalary() int // 這是一個函式,可以有多個接受型別
}
# 介面實現
func (p Permanent) CalculateSalary() int {
	return p.basicpay + p.pf  // Permanent是basicpay+pf
}
func (c Contract) CalculateSalary() int {
	return c.basicpay // Contract
}
# Permanent和Contract使用的是2個結構體
type Permanent struct {
	empId    int
	basicpay int
	pf       int
}
type Contract struct {
	empId    int
	basicpay int
}
# 賦值給結構體
pemp1 := Permanent{1, 5000, 20} // Permanent是basicpay+pf
cemp1 := Contract{3, 3000} // Contract是basicpay
# 那麼計算多個pempN和cempN就會變得容易
employees := []SalaryCalculator{pemp1, cemp1} // 建立一個介面型別 SalaryCalculator 的切片
totalExpense(employees) // 呼叫計算函式
# 
func totalExpense(s []SalaryCalculator) { // 接收 介面型別
	expense := 0
	for _, v := range s {
		expense = expense + v.CalculateSalary() // 累計數量
	}
	fmt.Printf("Total Expense Per Month $%d", expense)
}

# 介面的內部表示
# 我們可以把介面看作內部的一個元組 (type, value)。 type 是介面底層的具體型別(Concrete Type),而 value 是具體型別的值。
# 例子
package main
import (  
    "fmt"
)
type Test interface {  // Test 介面
    Tester()
}
type MyFloat float64
func (m MyFloat) Tester() {  // MyFloat 型別實現了該介面
    fmt.Println(m)
}
func describe(t Test) {  
    fmt.Printf("Interface type %T value %v\n", t, t)
}

func main() {  
    var t Test // t 為介面型別
    f := MyFloat(89.7) // f 為MyFloat型別
    t = f // 把MyFloat型別賦給Test介面型別
    describe(t) // Interface type main.MyFloat value 89.7
    t.Tester() // 89.7 t的MyFloat型別呼叫t的Test介面,以及實現Tester()
}

# 空介面
# 沒有包含方法的介面稱為空介面。空介面表示為 interface{}。由於空介面沒有方法,因此所有型別都實現了空介面。
# 這個函式可以傳遞任何型別。
func describe(i interface{}) {  
    fmt.Printf("Type = %T, value = %v\n", i, i)
}
s := "Hello World"
describe(s)
i := 55
describe(i)
strt := struct { // 匿名結構體
    name string
}{
    name: "Naveen R", // 直接賦值
}
describe(strt)

# 型別斷言
# 型別斷言用於提取介面的底層值(Underlying Value)。在語法 i.(T) 中,介面 i 的具體型別是 T,該語法用於獲得介面的底層值。
package main

import (  
    "fmt"
)

func assert(i interface{}) {  
    v, ok := i.(int) // 檢測是不是有int型別
    fmt.Println(v, ok) // 輸出v
}
func main() {  
    var s interface{} = 56
    assert(s)
    var i interface{} = "Steven Paul"
    assert(i)
}
# 型別選擇(Type Switch)
# 型別選擇用於將介面的具體型別與很多 case 語句所指定的型別
func findType(i interface{}) {  
    switch i.(type) {
    case string:
        fmt.Printf("I am a string and my value is %s\n", i.(string))
    case int:
        fmt.Printf("I am an int and my value is %d\n", i.(int))
    default:
        fmt.Printf("Unknown type\n")
    }
}
func main() {  
    findType("Naveen")
    findType(77)
    findType(89.98) // 89.98 的型別是 float64,沒有在 case 上匹配成功
}

介面(二)

# 我們在討論方法的時候就已經提到過,使用值接受者宣告的方法,既可以用值來呼叫,也能用指標呼叫。不管是一個值,還是一個可以解引用的指標,呼叫這樣的方法都是合法的。
# 介面就是呼叫方法,也是一樣的。
package main

import "fmt"

type Describer interface {
	Describe()
}
type Person struct {
	name string
	age  int
}

func (p Person) Describe() { // 使用值接受者實現
	fmt.Printf("%s is %d years old\n", p.name, p.age)
}

type Address struct {
	state   string
	country string
}

func (a *Address) Describe() { // 使用指標接受者實現
	fmt.Printf("State %s Country %s", a.state, a.country)
}

func main() {
	var d1 Describer        // 介面型別
	p1 := Person{"Sam", 25} // 把結構體賦值
	d1 = p1                 // 通過不同的結構體使用介面的不同方法
	d1.Describe()           // 既可以用值來呼叫,也能用指標呼叫
	d2 := &p1               // 通過不同的結構體使用介面的不同方法
	d2.Describe()           // 既可以用值來呼叫,也能用指標呼叫

	var p2 Describer
	a := Address{"Washington", "USA"}
	a.Describe() // 這樣是可以的
	//p2 = a       // 不可以,對於使用指標接受者的方法,用一個指標或者一個可取得地址的值來呼叫都是合法的。 cannot use a (type Address) as type Describer in assignment:Address does not implement Describer (Describe method has pointer receiver)
	//但是介面中儲存的具體值(Concrete Value)並不能取到地址,因此,對於編譯器無法自動獲取 a 的地址,於是程式報錯。
	//func (a *Address) Describe()去掉*
	p2 = &a
	p3 := a
	p2.Describe()
	p3.Describe()
}
# 輸出
# Sam is 25 years old
# Sam is 25 years old
# State Washington Country USA
# State Washington Country USA
# State Washington Country USA

# 實現多個介面
e := Employee{ // 一個結構體
	firstName:   "Naveen",
	lastName:    "Ramanathan",
	basicPay:    5000,
	pf:          200,
	totalLeaves: 30,
	leavesTaken: 5,
}
// SalaryCalculator和LeaveCalculator介面
// DisplaySalary和DisplaySalary2實現介面
var s SalaryCalculator = e
s.DisplaySalary() // 呼叫就行
var l LeaveCalculator = e
l.DisplaySalary2() // 呼叫就行

# 介面的巢狀
# 儘管 Go 語言沒有提供繼承機制,但可以通過巢狀其他的介面,建立一個新介面。
type SalaryCalculator interface {
	DisplaySalary()
}

type LeaveCalculator interface {
	CalculateLeavesLeft() int
}

type EmployeeOperations interface { // 巢狀2個介面
	SalaryCalculator
	LeaveCalculator
}
// 實現
func (e Employee) DisplaySalary() {
	fmt.Printf("%s %s has salary $%d", e.firstName, e.lastName, (e.basicPay + e.pf))
}
// 實現
func (e Employee) CalculateLeavesLeft() int {
	return e.totalLeaves - e.leavesTaken
}
// 一樣的使用
e := Employee{
	firstName:   "Naveen",
	lastName:    "Ramanathan",
	basicPay:    5000,
	pf:          200,
	totalLeaves: 30,
	leavesTaken: 5,
}
var empOp EmployeeOperations = e
empOp.DisplaySalary() // 使用SalaryCalculator介面的方法

# 介面的零值
# 介面的零值是 nil。對於值為 nil 的介面,其底層值(Underlying Value)和具體型別(Concrete Type)都為 nil。
type Describer interface {  
    Describe()
}
func main() {  
    var d1 Describer
    if d1 == nil {
        fmt.Printf("d1 is nil and has type %T value %v\n", d1, d1)
    }
    d1.Describe() // 對於值為 nil 的介面,由於沒有底層值和具體型別,當我們試圖呼叫它的方法時,程式會產生 panic 異常。
}

到了這裡時候將開始學習更加特色的golang


併發

# Go 是併發式語言,而不是並行式語言。
# 併發是指立即處理多個任務的能力。我們可以想象一個人正在跑步。假如在他晨跑時,鞋帶突然鬆了。於是他停下來,系一下鞋帶,接下來繼續跑。這個例子就是典型的併發。這個人能夠一下搞定跑步和繫鞋帶兩件事,即立即處理多個任務。
# 並行是指同時處理多個任務。我們同樣用這個跑步的例子來幫助理解。假如這個人在慢跑時,還在用他的 iPod 聽著音樂。在這裡,他是在跑步的同時聽音樂,也就是同時處理多個任務。這稱之為並行。
# 並行不一定會加快執行速度,因為並行執行的元件之間可能需要相互通訊。
# Go 程式語言原生支援併發。Go 使用 Go 協程(Goroutine) 和通道(Channel)來處理併發。

Go 協程

# Go 協程相比於執行緒的優勢
# 相比執行緒而言,Go 協程的成本極低。堆疊大小隻有若干 kb,並且可以根據應用的需求進行增減。而執行緒必須指定堆疊的大小,其堆疊是固定不變的。
# Go 協程會複用(Multiplex)數量更少的 OS 執行緒。即使程式有數以千計的 Go 協程,也可能只有一個執行緒。如果該執行緒中的某一 Go 協程發生了阻塞(比如說等待使用者輸入),那麼系統會再建立一個 OS 執行緒,並把其餘 Go 協程都移動到這個新的 OS 執行緒。所有這一切都在執行時進行,作為程式設計師,我們沒有直接面臨這些複雜的細節,而是有一個簡潔的 API 來處理併發。
# Go 協程使用通道(Channel)來進行通訊。通道用於防止多個協程訪問共享記憶體時發生競態條件(Race Condition)。通道可以看作是 Go 協程之間通訊的管道。我們會在下一教程詳細討論通道。

# 啟動一個 Go 協程
# 呼叫函式或者方法時,在前面加上關鍵字 go,可以讓一個新的 Go 協程併發地執行。
package main
import (
	"fmt"
)
func hello() {
	fmt.Println("Hello world goroutine")
}
func main() {
	// go hello() 啟動了一個新的 Go 協程。現在 hello() 函式與 main() 函式會併發地執行。
	// 主函式會執行在一個特有的 Go 協程上,它稱為 Go 主協程(Main Goroutine)
	go hello()
	fmt.Println("main function")
}
# 輸出
# main function
# 即hello()沒有執行
# 啟動一個新的協程時,協程的呼叫會立即返回。與函式不同,程式控制不會去等待 Go 協程執行完畢。在呼叫 Go 協程之後,程式控制會立即返回到程式碼的下一行,忽略該協程的任何返回值。
# 如果希望執行其他 Go 協程,Go 主協程必須繼續執行著。如果 Go 主協程終止,則程式終止,於是其他 Go 協程也不會繼續執行。
# 修復這個問題
package main
import (
	"fmt"
	"time"
)
func hello() {
	fmt.Println("Hello world goroutine")
}
func main() {
	go hello()
	// 延遲主協程結束時間
	time.Sleep(1 * time.Second)
	fmt.Println("main function")
}

# 啟動多個 Go 協程
# 通過延遲時間進行的交替執行
package main

import (
	"fmt"
	"time"
)

func numbers() {
	for i := 1; i <= 5; i++ {
		time.Sleep(250 * time.Millisecond)
		fmt.Printf("%d ", i)
	}
}
func alphabets() {
	for i := 'a'; i <= 'e'; i++ {
		time.Sleep(400 * time.Millisecond)
		fmt.Printf("%c ", i)
	}
}
func main() {
	go numbers()
	go alphabets()
	time.Sleep(3000 * time.Millisecond)
	fmt.Println("main terminated")
}
# 輸出
# 1 a 2 3 b 4 c 5 d e main terminated

通道

# 通道可以想像成 Go 協程之間通訊的管道。如同管道中的水會從一端流到另一端,通過使用通道,資料也可以從一端傳送,在另一端接收。
# 通道的宣告,所有通道都關聯了一個型別。通道只能運輸這種型別的資料,而運輸其他型別的資料都是非法的。
var a chan int // 通道的宣告
if a == nil {
   fmt.Println("channel a is nil, going to define it")
   a = make(chan int) // 通道的零值為 nil。通道的零值沒有什麼用,應該像對 map 和切片所做的那樣,用 make 來定義通道。
   fmt.Printf("Type of a is %T", a)
}

# 簡短宣告通常也是一種定義通道的簡潔有效的方法
a := make(chan int)
# 通過通道進行傳送和接收
# 傳送與接收預設是阻塞的
data := <- a // 讀取通道 a , 箭頭對於 a 來說是向外指的,因此我們讀取了通道 a 的值,並把該值儲存到變數 data。
a <- data // 寫入通道 a, 箭頭指向了 a,因此我們在把資料寫入通道 a
# 例子
package main

import (
	"fmt"
)

func hello(done chan bool) {
	fmt.Println("Hello world goroutine")
	done <- true // 寫入資料 bool 型別,接下來向 done 寫入資料。當完成寫入時,Go 主協程會通過通道 done 接收資料,於是它解除阻塞狀態,打印出文字 main function。
}
func main() {
	done := make(chan bool) // 型別的管道done
	go hello(done)
	<-done // 通過通道 done 接收資料。這一行程式碼發生了阻塞,除非有協程向 done 寫入資料,否則程式不會跳到下一行程式碼。於是,這就不需要用以前的 time.Sleep 來阻止 Go 主協程退出了。
	// 主協程發生了阻塞,等待通道 done 傳送的資料。
	fmt.Println("main function")
}
# 該程式會計算一個數中每一位的平方和與立方和,然後把平方和與立方和相加並打印出來。
package main

import (
	"fmt"
)

func calcSquares(number int, squareop chan int) {
	sum := 0
	for number != 0 {
		digit := number % 10
		sum += digit * digit
		number /= 10
	}
	squareop <- sum // 寫入通道值
}

func calcCubes(number int, cubeop chan int) {
	sum := 0
	for number != 0 {
		digit := number % 10
		sum += digit * digit * digit
		number /= 10
	}
	cubeop <- sum // 寫入通道值
}

func main() {
	number := 589
	sqrch := make(chan int)
	cubech := make(chan int)
	go calcSquares(number, sqrch)
	go calcCubes(number, cubech)
	squares, cubes := <-sqrch, <-cubech // 阻塞,等待通道數值
	fmt.Println("Final output", squares+cubes)
}

# 死鎖
# 使用通道需要考慮的一個重點是死鎖。當 Go 協程給一個通道傳送資料時,照理說會有其他 Go 協程來接收資料。如果沒有的話,程式就會在執行時觸發 panic,形成死鎖。

# 單向通道
# 這種通道只能傳送或者接收資料。
sendch := make(chan<- int) // 定義了唯送通道,因為箭頭指向了 chan
# 需要 通道轉換
func sendData(sendch chan<- int) { //  轉換為一個唯送通道
	sendch <- 10
}
func main() {
	cha1 := make(chan int) // 雙向通道
	go sendData(cha1)
	fmt.Println(<-cha1)
}
// 於是該通道在 sendData 協程裡是一個唯送通道,而在 Go 主協程裡是一個雙向通道。

# 關閉通道和使用 for range 遍歷通道
# 如果成功接收通道所傳送的資料,那麼 ok 等於 true。而如果 ok 等於 false,說明我們試圖讀取一個關閉的通道。從關閉的通道讀取到的值會是該通道型別的零值。例如,當通道是一個 int 型別的通道時,那麼從關閉的通道讀取的值將會是 0。
v, ok := <- ch
# 例子
func producer(chnl chan int) {
	for i := 0; i < 10; i++ {
		chnl <- i // 增加了很多通道的值
	}
	close(chnl) // 關閉通道,不然會死鎖
}
func main() {
	ch := make(chan int)
	go producer(ch)
	for { // 無限迴圈
		v, ok := <-ch
		if ok == false {
			break // 取值到關閉通道的位置,就跳出去
		}
		fmt.Println("Received ", v, ok) // 遍歷列印ok的通道值
	}
}
// 使用range
func main() {  
    ch := make(chan int)
    go producer(ch)
    for v := range ch { // ch是很多通道的值,需要使用close()關閉,不然死鎖
        fmt.Println("Received ",v)
    }
}

緩衝通道和工作池(Buffered Channels and Worker Pools)

# 建立一個有緩衝(Buffer)的通道。只在緩衝已滿的情況,才會阻塞向緩衝通道(Buffered Channel)傳送資料。同樣,只有在緩衝為空的時候,才會阻塞從緩衝通道接收資料。
# 同樣,只有在緩衝為空的時候,才會阻塞從緩衝通道接收資料。
# 通過向 make 函式再傳遞一個表示容量的引數(指定緩衝的大小),可以建立緩衝通道。
ch := make(chan type, capacity) // capacity無緩衝通道的容量預設為 0
# 例子
func main() {
	ch := make(chan string, 2) // 建立容量為 2 的通道
	ch <- "naveen"             // 不會發生阻塞
	ch <- "paul"               // 不會發生阻塞
	// ch <- "three"           // 死鎖,容量為2的通道只能接受2個寫入,提前fmt.Println(<-ch)讀取,釋放快取就能再次寫入了
	fmt.Println(<-ch)
	fmt.Println(<-ch)
}
# 例子
func write(ch chan int) {
	for i := 0; i < 5; i++ {
		ch <- i // ch的快取是2,當寫完2個的時候,就會發生阻塞,直到 ch 內的值被讀取
		fmt.Println("successfully wrote", i, "to ch")
	}
	close(ch) // 必須關掉通道
}
func main() {
	ch := make(chan int, 2)
	go write(ch)
	time.Sleep(2 * time.Second)
	for v := range ch { // ch 內的值被讀取,快取被釋放,ch <- i就又可以進行寫入了
		fmt.Println("read value", v, "from ch")
		time.Sleep(2 * time.Second)
	}
}

# 長度 vs 容量
# 緩衝通道的容量是指通道可以儲存的值的數量。我們在使用 make 函式建立緩衝通道的時候會指定容量大小。
# 緩衝通道的長度是指通道中當前排隊的元素個數。
ch := make(chan string, 3)
ch <- "naveen"
ch <- "paul"
fmt.Println("capacity is", cap(ch)) // capacity is 3
fmt.Println("length is", len(ch))   // length is 2
fmt.Println("read value", <-ch)     //讀取釋放快取
fmt.Println("length is", len(ch))   // length is 1

# WaitGroup
# WaitGroup 用於等待一批 Go 協程執行結束。程式控制會一直阻塞,直到這些協程全部執行完畢。
# 例子
package main

import (
	"fmt"
	"sync"
	"time"
)

func process(i int, wg *sync.WaitGroup) { // 接受 wg 的地址
	fmt.Println("started Goroutine ", i)
	time.Sleep(2 * time.Second)
	fmt.Printf("Goroutine %d ended\n", i)
	(wg).Done() // 減少計數器,
}

func main() {
	no := 3
	var wg sync.WaitGroup // 建立了 WaitGroup 型別的變數,其初始值為零值
	for i := 0; i < no; i++ {
		wg.Add(1)          // WaitGroup 使用計數器來工作。當我們呼叫 WaitGroup 的 Add 並傳遞一個 int 時,WaitGroup 的計數器會加上 Add 的傳參。
		go process(i, &wg) // 這裡的計數器會變成3,同時建立3個go協程,傳遞 wg 的地址是很重要的。如果沒有傳遞 wg 的地址,那麼每個 Go 協程將會得到一個 WaitGroup 值的拷貝,因而當它們執行結束時,main 函式並不會知道。
	}
	wg.Wait() // Wait() 方法會阻塞呼叫它的 Go 協程,這裡會等待3個go協程的wg.Done()減去直到計數器變為0
	fmt.Println("All go routines finished executing")
}

# 工作池的實現
# 緩衝通道的重要應用之一就是實現工作池。
# 一般而言,工作池就是一組等待任務分配的執行緒。一旦完成了所分配的任務,這些執行緒可繼續等待任務的分配。
# 我們會使用緩衝通道來實現工作池。

# 建立一個 Go 協程池,監聽一個等待作業分配的輸入型緩衝通道。
# 將作業新增到該輸入型緩衝通道中。
# 作業完成後,再將結果寫入一個輸出型緩衝通道。
# 從輸出型緩衝通道讀取並列印結果。
# https://studygolang.com/articles/12512