Golang快速入門學習筆記
阿新 • • 發佈:2018-11-19
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