基礎及關鍵字
-
if for switch都支持使用隱形聲明(:=)來快速聲明一個變量,無需在上面一行額外聲明,這可以增加代碼簡潔性,但不太符合其他常規語言的寫法,需要習慣一下
-
if for switch都不需要使用()包裹表達式,只使用空格隔開就行
-
大寫的函數或變量才算是對外聲明(用開頭大寫來代替其他語言中的
public private\export
的概念) -
基礎類型轉換是顯式的,使用類型本身加(),值寫在括號中,即可完成類型轉換
-
導包和聲明變量都支持使用()批量聲明,這可以增加代碼簡潔性,也可以像其他語言一樣一條條聲明,也沒問題
-
switch的case匹配到后就直接停止,不會像其他語言一樣一路向后繼續匹配,除非以
fallthrough
語句結束 -
for關鍵字支持省略聲明、條件、表達式三個部分,這直接代替別的語言中的while了
-
聲明變量、方法參數、方法返回值時當多個參數類型重復時都支持省略類型,這可以有效減少重復代碼,但也容易出bug,寫法:
x, y, z int
-
go中各種表達式都使用分號;隔開
-
方法return支持多值return,只要方法的return類型定義時多定義幾個就好了,小括號括起來,用逗號隔開
-
盡量避免使用隱式return(方法需要返回值時,直接一個return將函數中的所有變量都返回掉),因為隱式return容易出現問題(太依賴編譯器,函數后續不好修改)
-
go中的返回值也支持聲明類型的同時給其命名,這就更能說明不要使用隱式return了,容易出現問題
-
循環中使用
break
跳出 -
defer關鍵字是defer語句所在的函數運行完成后再調用被defer修飾的代碼,defer調用的函數會被壓棧,會遵從后進先出的順序依次調用
-
以下函數很違反直覺:
package mainimport "fmt"func main() {fmt.Println("counting")defer fmt.Println("done")i := 0;for ; i < 10; i++ {defer fmt.Println(i)}
}
結果:
counting
9
8
7
6
5
4
3
2
1
0
done
指針
- 指針直接指向內存地址,操作指針等于操作該值本身。
- 使用
&
對某個變量產生一個指針:&i
,使用*
修飾T(類型)來聲明一個指針:p *int
,使用*p
的方式來解引用指針(使用指針)。 &
表示取地址,A變量被取地址后賦值給新的變量B,這個變量B就成為了變量A的指針,直接打印變量B時只會打印出內存地址*
表示解引用,變量B目前是指針,使用*
對變量B進行解引用,即*B
就可以打印原來的變量A的值,解引用后的*B
和原來的A的值是完全一樣的,即內存上存儲使用的是同一份。- 列表指針寫法:
*List[T]
,指針的指針寫法:**List[T]
,指針的指針是要修改的變量的指針的指針,不常用但是在一些特殊場景很有用,通常用在替換要修改的變量的指針本身時使用,口述比較繞,但是看一個關于鏈表的代碼例子就明白了:
// 在 main 函數中,我們的鏈表頭是一個指針
var listHead *List[T] = nil // 一開始是空鏈表// 我們希望 Push 函數能修改 listHead 這個指針,讓它指向一個新的節點。
func (l **List[T]) Push(v T) {// 如果我們只接收 *List[T],那么 l 只是 listHead 的一個副本。// 我們需要修改 listHead 本身,所以必須接收它的地址,即 **List[T]。// *l 的意思就是:“通過這把鑰匙,拿到 main 函數里的那張原始藏寶圖(listHead 指針)”// 然后我們修改這張原始藏寶圖,讓它指向一個新的寶藏(新節點)。*l = &List[T]{next: *l, val: v}
}
- 黃金法則:想修改什么,就向函數傳遞它的指針,不論是變量還是指針本身
- 直接給指針本身賦值時,必須也要賦值指針,不能賦值值本身
結構體
- go中聲明結構體(類似于類)使用
type 結構體名稱 struct
加大括號:
type Vertex struct {X intY int
}
- 實例化結構體:
v := Vertex{1, 2}//允許如下方式實例化結構
v := Vertex{X: 1} //對X賦值使用冒號而不是等號,這里Y就是0,println(v)打印結果是 {1 0}
- 配合結構體使用:
p := &v
等價于
var p *Vertex = &v
go允許隱式解引用,即直接省略和星號和大括號對結構體引用進行使用:
p.X = 1e9
等價于
(*p).X = 1e9
數組和切片
- 數組聲明方式:
var a [2]string
,與其他語言不同的是數量和中括號放在了類型前邊 - 初始化數組的方式:
var a [2]string
,帶初始值的方式:a := [2]string{"hello", "world"}
或者var c [2]string = [2]string{"hello", "world"}
- 數組和切片的區別是數組聲明時必須指定長度,而切片則由編譯器推導。數組是實際存儲值的,而切片不存儲值(只是相當于數組中某段元素的引用),切片更常用
- 切片遵從要頭不要尾,
a[1:4]
表示取數組的1,2,3下標的元素,不包含4 - 切片類似于數組的引用,即它描述了數組中的某段元素,修改切片等于直接修改數組本身
- 構建數組時中括號
[]
里不寫數字,就相當于構建了一個數組+切片,注意,這樣的話長度其實仍然固定,如果直接給超過聲明長度的下標賦值或取值就會報越界錯誤 - 切片可以忽略上界和下界:
//切片下界的默認值為 0,上界則是該切片的長度。//對于數組 var a [10]int 來說,以下切片表達式和它是等價的:a[0:10]
a[:10]
a[0:]
a[:]
make
(內置函數)用來創建動態數組,比較常用:
//第三個參數cap可以省略,也可以手動指定
b := make([]int, 0, 5) // len(b)=0, cap(b)=5
- 數組的len可以理解為數組長度,cap可以理解為容量。容量總是大于等于長度的。
- 切片的類型可以是任意類型包括結構體或切片等,例如
[][]string
,[]struct
- append(內置函數)用來為切片追加值:
v := []int{0,1} //聲明切片
v = append(v, 2, 3, 4) //追加多個值
fmt.Println(v) //結果:[0 1 2 3 4]
- 使用range關鍵字遍歷切片或數組
- 在go中利用切片結合uint8的對應灰度值來渲染一張圖片,關鍵點就是對切片本身的使用:
package mainimport (
"golang.org/x/tour/pic"
)func Pic(dx, dy int) [][]uint8 {outerSlice := make([][]uint8 ,dy)for y := range outerSlice {innerSlice := make([]uint8, dx)for x := range innerSlice {v := x*x+y*y //改變這里可以改變結果圖片的效果,例如:(x+y)/2、x*y、x^y、x*log(y) 、x%(y+1)、x*x、y*y 、x * math.Log(float64(y))等innerSlice[x] = uint8(v)}outerSlice[y] = innerSlice}return outerSlice
}func main() {pic.Show(Pic)
}
map(映射)
- 創建map可以使用make函數,聲明map類型時的格式如:
map[string]string
,第一個中括號中的為鍵類型可以是任意類型,通常是string,后面緊跟著的是值類型,可以是任意類型 - map創建時后面的大括號類似于kotlin中的mapOf,不過把to換成了冒號
- 通過雙賦值表達式檢測鍵是否存在:
v, exsit := m["答案"]
fmt.Println("值:", v, "是否存在?", exsit)
函數(方法)
- 函數可以作為參數傳遞,參數類型為func,函數可以賦值給一個變量,該變量即成為函數本身,跟js中的函數特點有點像,函數類型也可以作為另一個函數的返回值
- 函數的閉包通常可以用于統計和累計,這很有用,可以省去一些不必要的外部全局變量
- 一段不正確的斐波那契數列計算代碼,這段代碼跳過了開頭的0和1的輸出:
package mainimport "fmt"// fibonacci 是返回一個「返回一個 int 的函數」的函數
func fibonacci() func() int {r := []int{0,1}return func () int {sum := r[len(r)-1] + r[len(r)-2]r = append(r,sum)//fmt.Println(r)//fmt.Println( r[len(r)-1:len(r)][0] )return sum}
}func main() {f := fibonacci()for i := 0; i < 10; i++ {fmt.Print(f()," ")}
}//會打印 :1 2 3 5 8 13 21 34 55 89
修正思路為每次調用時,第一個值正是要返回的結果,而后只使用最新的兩個數字來保持狀態值最新,下面是修正后的函數:
func fibonacci() func() int {a,b := 0,1return func () int {ret := aa,b = b,a+breturn ret}
}//打印:0 1 1 2 3 5 8 13 21 34
- 為結構體(或基礎類型)定義方法的方式就是使用括號包含具體的帶名稱的類型,類似于kotlin的擴展方法,但是不能定義在結構體內,也必須得聲明一個名稱來引用而不是this
//定義
type Vertex struct {X, Y float64
}func (v Vertex) Abs() float64 {return math.Sqrt(v.X*v.X + v.Y*v.Y)
}//使用
v := Vertex{3, 4}
fmt.Println(v.Abs())
為類型定義方法的方式是使用type轉一下類型:
//定義
type MyFloat float64func (f MyFloat) Abs() float64 {if f < 0 {return float64(-f)}return float64(f)
}//使用
f := MyFloat(-math.Sqrt2)
fmt.Println(f.Abs())
- 不能跨包定義方法
- 為指針類型接收者定義方法通常更常用,因為有時需要通過該方法來修改結構體的數據并使其在后續生效:
package mainimport ("fmt"
)type Vertex struct {A,B int
}//沒有對v(指針型類型接收者)進行修改的能力
func (v Vertex) Sum() int {return v.A+v.B
}//有對v進行修改的能力
func (v *Vertex) Scale(l int) {v.A = v.A * lv.B = v.B * l
}//使用
func main() {v := Vertex{3, 4}v.Scale(10)fmt.Println(v.Sum())
}//打印70
- 如果不確定,或者想修改接收者,用指針接收者通常是更安全、更通用的選擇,定義為指針接收者通常對性能也有很大的幫助,可以避免復制開銷
- go的語法糖:1.定義為指針型接收者的方法,在調用時也直接允許值類型的進行調用 2.定義為值型接受者的方法,在調用時也允許指針類型進行調用
- 當一個方法實現了接口或者作為結構體的方法,有入參,有多個響應值,且響應值中有方法類型時,這個方法看起來會很奇怪,有多達4個或更多的括號,但是卻有效合法,如下復雜示例:
package mainimport "fmt"type Vertex struct{X,Y int
}//Vertex類型的專屬異常
type VertexError struct {V Vertex
}//Vertex類型的專屬異常處理
func (e *VertexError) Error() string {return fmt.Sprintf("vertex error on value: %+v", e.V)
}//復雜函數
func (v *Vertex) Sum(scale func(int) int) (int,(func(int) int), error){if v.X == -1 {return 0, nil, &VertexError{*v}}return scale(v.X) + scale(v.Y),scale,nil
}func Scale(s int) int {return s * s
}func main() {v := Vertex{-1,2} //-1時會觸發異常s,f,err := v.Sum(Scale)if err != nil {fmt.Printf("error: %v",err)return}fmt.Printf("s: %v f: %v",s,f)
}
接口
- go中的接口使用interface關鍵字,沒有顯示的類似于其他語言中的"implements"或冒號等定義,某個類型的方法只要包含目標接口的全部方法,就可以說該類型實現了目標接口,沒有代碼上顯式的引用和定義關系
- 弊端是無法一眼看出某個類型到底實現了某個接口沒有,好處就是代碼靈活
- 接口也是值,可以在方法參數、返回值、變量等進行傳遞
- 空接口是一個特殊定義:
interface {}
,可以保存任何類型的值內容,類似于kotlin中的Any?或java中的Object+null類型 - 類型斷言,格式和寫法:
i.(string)
,具體用法如下,可以檢查出該接口底層保存的值以及是否使用的是對應的類型:
var i interface{} = "hello"s := i.(string)fmt.Println(s) //hellos, ok := i.(string)fmt.Println(s, ok) //hello truef, ok := i.(float64)fmt.Println(f, ok) //0 falsef = i.(float64) // panicfmt.Println(f) //panic: interface conversion: interface {} is string, not float64
用來檢查并獲取值確實不錯,看起來挺好用的
- 類型選擇,格式和寫法:
i.(type){ case ... }
, 類型選擇的寫法如下,可以通過類似switch的方式檢查對應接口值是否是該類型:
package mainimport "fmt"func do(i interface{}) {switch v := i.(type) {case int:fmt.Println("是int類型")case string:fmt.Println("是string類型")default:fmt.Println("未知類型", v)}
}func main() {do(21) //是int類型do("hello") //是string類型do(true) //未知類型 true
}
- 方法調用的返回值可以主動的響應error,通過判斷error是否為空:
if err != nil
來進行錯誤處理:
package mainimport ("errors""fmt"
)func Div(a, b int) (int, error) {if b == 0 {return 0, errors.New("b is 0!") //拋出一個錯誤}return a / b, nil
}func main() {div, err := Div(3, 0)if err != nil {fmt.Println(err) //b is 0!return //提前返回}fmt.Println(div)
}
- error是一個內置接口,可以輕松使用方法對某個類型定義特定的錯誤處理,實現:
Error() string
方法即可 - 錯誤處理完后應當立即返回,通常使用return或者log.Fatal
- 實現接口可以做很多事情,一種常見的好處就是解耦且不用關注額外細節就能實現強大的功能,因為方法參數如果是接口類型的,那么傳入的實例只要能實現對應接口的所有方法,就可以正確作為參數,至于方法內部如何,應該根據方法實際情況來完成實現
- 實現了圖片接口的結構體,可以借助pic庫完成圖片繪制,耦合性很低,表現力很高,示例:
package mainimport ("golang.org/x/tour/pic""image""image/color"
)// Image 是我們將要定義的自定義圖片類型。
// 它是一個空結構體,因為我們不需要存儲任何數據。
// 圖像的像素是動態計算出來的。
type Image struct{}// Bounds 方法返回圖像的尺寸。
// 我們定義一個 256x256 像素的圖像。
func (i Image) Bounds() image.Rectangle {return image.Rect(0, 0, 256, 256)
}// ColorModel 返回圖像的顏色模型。
// 我們使用標準的 RGBA 模型。
func (i Image) ColorModel() color.Model {return color.RGBAModel
}// At 方法是核心。它在給定的 (x, y) 坐標處返回一個顏色。
// 渲染程序會為圖像中的每一個像素調用一次此方法。
func (i Image) At(x, y int) color.Color {// 使用 x 和 y 坐標通過一個簡單的函數生成一個值。// 這里的 x^y 是按位異或(XOR)操作,能產生有趣的圖案。v := uint8(x ^ y)// 返回一個 RGBA 顏色。// R(紅)= v, G(綠)= v, B(藍)= 255, A(透明度)= 255(不透明)// 這會產生一個藍色的、帶有漸變紋理的圖像。return color.RGBA{v, v, 255, 255}
}func main() {// 創建我們自定義 Image 類型的一個實例。m := Image{}// pic.ShowImage 會接收任何實現了 image.Image 接口的類型,// 并將其渲染成圖片(在 Go Tour 中是 base64 編碼的 PNG)。pic.ShowImage(m)
}
類(泛)型參數
- 類似于其他語言中的泛型
- 聲明方式:
func Index[T comparable](s []T, x T) int
,這表示s的元素類型可以是滿足了comparable
接口的所有類型,x 的類型也是滿足了 comparable接口的所有類型
雜項
- 遍歷鏈表:
//常見的需要取值的標準寫法:
for n := l; n != nil; n = n.next//將鏈表撥動到最后位置的寫法:
current := *l
for current.next != nil {current = current.next
}
- 構建字符串使用strings.Builder,方法是WriteString,獲取結果是string()