1. 程式人生 > 其它 >Go基礎知識總結

Go基礎知識總結

1. 變數

變數的宣告有四種方式:

  1. 宣告一個變數,預設的初始化值為0:

    var a int

  2. 宣告一個變數,初始值為100:

    var a int = 100

  3. 初始化時候省略資料型別,通過值自動推導變數的資料型別:

    var a = 100

  4. 省略掉var關鍵字,直接自動匹配,但要使用:=

    a := 100

一個注意的點:第四種宣告變數的方式a := 100只能在區域性方法中使用,全域性變數不支援這種寫法

多個變數一起宣告的寫法:

  1. 單行寫法

    var a, b int = 100, 200

    var a, b = 100, "abc"

    a, b := 100, "abc"

  2. 多行寫法

    var (
    	a int = 100
    	b string = "abc"
    )
    

匿名變數

go中使用下劃線_來作為匿名變數。

go支援函式多返回值,而當我們對於某個函式的返回值是不關心的時候,可以使用匿名變數來接收

比如:fd, _ := os.Open(xxx),對於第二個返回值我們並不想要,就可以直接用_接收

2. 常量

go中常量使用關鍵字const

定義常量與定義變數方式類似,只是將關鍵字var換成了const,但常量定義沒有:=這種寫法

比如:

const a int = 100

const (
	a = 10
	b = 20
)

3. iota關鍵字

iota用於與const表示列舉型別

go中定義列舉使用的是iotaconst,如下程式碼,定義一個列舉

const (
	RED = iota
    BLUE
    BLACK
    ....
)

注意:在const中新增一個關鍵字iota,每一行的iota都會累加1,第一行的iota預設值是0

因此上面的,RED=0,BLUE=1,BLACK=2

但是如果第一行的RED我們賦值為5 * iota,那麼RED=5 * 0=0,BLUE=5 * 1=5,BLACK=5 * 2=10

因為每一行的iota自動累加1,每一行相當於是5 * iota

因此有一個常見的例項,使用iota來進行左移運算實現儲存單位的常量列舉:

const (
	_ = iota // 賦值給_忽略這個值
    B = 1 << (10 * iota)
    KB
    MB
    GB
    TB
    ...
)

4.函式

go函式是允許有多個返回值的。go的函式定義可以有以下幾種寫法:

  1. 返回多個值,使用匿名變數

    func test(a string, b int) (int, int) {
        ....
        
        return 100, 200
    }
    
  2. 返回多個值,有引數名稱的

    func test(a string, b int) (c, d int) {
        ...
        c = 100
        d = 200
        
        return
    }
    

    注意:

    1. c和d屬於test方法的形參,初始值預設為0,他們的作用空間也僅限於test方法,當已經給返回值變數賦值後,可以直接return就好了。

    2. 也可以返回別的變數, 比如內部在定義一個 e := 300,最後 return c, e

5. init函式

init函式是go在每個包初始化後自動執行的,而且在main函式之前執行

因此,init函式常用來:對變數初始化,註冊等。

init函式的幾個特點:

  1. init函式用於包的初始化,是在package xxxx的時候完成的,在main之前完成

  2. 每個包中是可以擁有多個init函式的,每個包的原始檔也是可以有多個init函式的

  3. 不同包的init函式是需要根據包匯入的依賴關係決定的(因為init是在package xxx之後完成)

    所以是類似棧的結構,最後的包的init方法先執行

  4. init函式不能被其他函式呼叫,也不需要傳入引數,也無返回值

package main

import "fmt"

func int() {
    fmt.Println("init ok")
}

func main() {
    fmt.Println("main...")
}

6. import 導包

go中使用import進行導包操作,有幾種情況需要了解下:

  1. import _ "fmt"

    這種使用_的方式,是給fmt包起一個別名,是一個匿名,這樣子會無法使用包中的方法,但是一旦導包,就會執行包裡的init()方法

  2. import aa "fmt"

    這種方式是給fmt包起一個別名aa,呼叫包中方法時候,就可以使用aa,比如aa.Println()

  3. import . "fmt"

    這種方式是將fmt包中的所有方法全部匯入到當前包中,那麼fmt包中的所有方法都可以直接當成本包的方法來呼叫了,不用再加包名fmt(但這樣本包就不能定義與fmt包所有函式的函式名相同的函數了)

7. defer

defer關鍵字是go獨有的,是一種延遲語句,在函式return前執行defer。

一個函式中可以新增多個defer語句,執行順序是逆序的,先定義的defer最後執行

一般defer用於資源的關閉操作比較多。

有個文章可以看看Golang中defer、return、返回值之間執行順序的坑

結論就是:return最先執行,return負責將結果寫入返回值中;接著defer開始執行一些收尾工作;最後函式攜帶當前返回值退出。

8. 陣列

  1. 宣告陣列的方式

    • var myArray1 [10]int
    • myArray1 := [5]int{1,2,3,4}
  2. 陣列長度是固定的

  3. 固定長度的陣列在傳參的時候,是嚴格匹配陣列型別的

    func add(array [4]int) {
    	fmt.Println(array[0], array[1], array[2], array[3])
    }
    func main() {
    	arr := [5]int{1, 2, 3, 4}
    	add(arr)
    }
    

    這樣子傳參是不行的,報錯: cannot use arr (variable of type [5]int) as type [4]int in argument to add,引數是[4]int型別,傳參是[5]int

  4. 需要注意的是,陣列是一個值型別,在賦值和作為引數傳遞時將產生一次複製動作。

9. 陣列切片(slice)

陣列切片slice,也叫動態陣列。

建立陣列切片有兩種方式:基於陣列和直接建立

  1. 基於陣列建立

    func main() {
        // 先定義一個數組
        var myArray [10]int = [10]int{1,2,3,4,5,6,7,8,9,10}
        // 基於陣列建立一個數組切片
        var mySlice []int = myArray[:5]
    }
    

    注意:go語言支援用myArray[first:last]這樣的方式基於陣列生成一個數組切片,這種[first,last]是左閉右開的。

    如果基於myArray的所有元素建立陣列切片:mySlice := myArray[:]

    基於myArray的前5個元素建立陣列切片:mySlice := myArray[:5]

    基於myArray的第5個元素開始到所有元素建立切片:mySlice := myArray[5:]

  2. 直接建立

    使用Go提供的內建函式make(),比如:

    • 建立一個初始元素個數為5的陣列切片,元素初始值為0:mySlice := make([]int, 5)

    • 建立一個初始元素個數為5的陣列切片,初始值為0,並預留10個元素的儲存空間:

      mySlice := make([]int, 5, 10)

元素的遍歷

  1. 使用len()函式獲取元素個數

    for i := 0; i < len(mySlice); i++ {
        ....
    }
    
  2. 使用range關鍵字遍歷

    for i, v := range mySlice {
        ....
    }// i 是index v是元素值
    

動態增減元素:

  1. 陣列切片支援內建函式cap()len()cap()函式返回的是陣列切片分配的空間大小,而len()函式返回的是陣列切片中當前所儲存的元素個數。

  2. 如果需要新增元素,可以使用append()函式,生成一個新的陣列切片

    mySlice = append(mySlice, 1, 2, 3)

    注意:

    1. 函式append()的第二個引數開始是一個不定引數,可以新增若干個元素

    2. 也可以將一個數組切片追加到另一個數組切片的末尾

      mySlice2 := []int{8, 9, 10}
      mySlice = append(mySlice, mySlice2...)
      

      這裡需要注意,第二個引數mySlice2後面加了三個點,也就是一個省略號,如果沒有這個省略號的話會編譯錯誤,因為append方法從第二個引數開始的所有引數都是待新增的元素,加上省略號相當於將mySlice2包含的元素逐個打散再加入

  3. 陣列切片擴容的機制

    在append的時候,如果長度增加後超過容量,比如mySlice := make([]int, 3, 4),切片mySlice的容量是4個,當前長度是3個元素,那麼在執行append,mySlice = append(mySlice, ,3, 4, 5)後,新增3個元素,加上之前的元素就總共有6個了,超過了容量4,所以這時候切片需要擴容,而擴容的機制就是原始容量的2倍,也就是在新增元素後發現超過了原始的容量的話,會自動以初始容量的2倍去擴容

  4. 切片複製

    使用內建函式copy(),用於將內容從一個數組切片複製到另一個數組切片。

    如果加入的兩個陣列切片沒有一樣大,就會按其中較小的那個陣列切片的元素個數進行復制。

    slice1 := []int{1, 2, 3, 4, 5}
    slice2 := []int{6, 7, 8}
    
    copy(slice2, slice2) // 只會複製slice1的前三個元素到slice2中
    // slice2 = {1,2,3}  slice1 = {1,2,3,4,5}
    
    copy(slice1, slice2) // 只會複製slice2的3個元素到slice1的前3個位置
    // slice2 = {6,7,8} slice1 = {6,7,8,4,5}
    
  5. 動態陣列在傳參上是引用傳遞的,而且不同元素長度的動態陣列他們的形參是一致的

    func printArray(myArray []int) {
        ...
    }
    

10. map

  1. map的宣告

    var myMap map[int]string

    其中myMap是變數名,int是鍵的型別,string是值的型別

    只宣告沒有建立的map還不可用!

  2. map的建立

    使用make()函式建立:myMap = make(map[int]string, 10)

    10表示的是map的容量,與切片的容量類似

  3. map的賦值

    • 可以先宣告,再建立,最後賦值

      var myMap map[int]string
      myMap = make(map[int]string, 10)
      myMap[0] = "java"
      myMap[1] = "Go"
      
    • 直接使用:=

      myMap := make(map[int]string)
      myMap[0] = "java"
      myMap[1] = "Go"
      
    • 宣告時賦值

      myMap := map[int]string{
          0: "java",
          1: "Go",
      }
      
  4. 元素刪除

    使用內建函式delete(),用於刪除容器內的元素

    delete(myMap, 0),第二個引數是鍵,如果這個鍵不存在,啥也不會發生,也不會有影響。

    但如果傳入的map是nil,則會丟擲異常panic

  5. 元素查詢

    從map中查詢一個特定的鍵,可以使用如下程式碼:

    value, ok := myMap[1]
    if ok { // 找到了
        ....
    }
    

    只需要檢視第二個返回值ok是否為true就知道找沒找到,不需要像其他語言那樣檢查取到的值是不是為nil

11. 面向物件

我們都知道面向物件三個特點:封裝,繼承,多型。

但是go中並不像其他面嚮物件語言那樣有很多的概念,go語言的面向物件程式設計是基於語言型別系統的,整個型別系統通過介面串聯。

1. 型別系統

go語言中的型別是可以新增方法的,可以給任何型別,包括內建型別增加新方法。比如:

type Integer int

func (a Integer) Less(b Integer) bool {
    return a < b
}

// 可以這樣使用
func main() {
    var a Integer = 1
    if a.Less(2) {
        fmt.Println(a, "Less 2")
    }
}

上面程式碼使用type定義了一個新的型別Integer,實質上它就是一個int型別,然後就給這個新型別增加了個新方法Less()。

新增方法這個語法可以以java類的概念來理解為:Integer就是一個類,而a就相當於類中的this,而Less是類裡的一個方法,當然a就可以呼叫到類裡的成員了,但是這裡的類實質是一個int,所以也就成員變數就是自身int值變量了,但如果a是一個結構體那就有成員變量了。

注意:當我們需要修改到物件的成員時,需要用到指標。比如程式碼修改為如下:

func (a *Integer) Add(b Integer) {
    *a += b
}

這裡需要修改到物件a的值,所以需要用指標引用。

如果沒有需要修改物件的值,go並不要求一定要用指標的,有時候物件很小,用指標傳遞反而不划算

其實上面用指標和不用指標的具體原因,歸根結底就是:Go語言的型別是基於值傳遞的,要修改變數的值,就需要傳遞指標。

2. 結構體

結構體的定義很簡單,基本和C一樣:

type Person struct {
    name string
    age int
}

// 新增一個方法
func (p *Person) setAge(age int) {
    p.age = age
}
func (p Person) getAge() {
    return p.age
}

當然,結構體也是go的一種型別,也是可以新增方法的,按我的理解,其實結構體就相當於是面向物件的類,新增的方法就是成員方法,而本身的成員變數就是類中的成員變數。

結構體初始化:

結構體初始化有以下幾種實現:

  1. p := new(Person)
  2. p := &Person{}
  3. p := &Person{"zhangsan", 18}
  4. p := &Person{name: "zhangsan", age: 20}

Go語言中沒有建構函式這種概念,物件的建立通常做法是交給一個全域性的建立函式來完成,以NewXXX命名,表示建構函式:

func NewPerson(name string, age int) *Person {
    return &Person{name, age}
}
3. 封裝

回到面向物件三要素,封裝,其實結構體就已經是封裝的實現了。

這裡有個注意的點就是:

類名,屬性名,方法名,首字母大寫表示對外(也就是其他包)可以訪問,否則只能在本包內訪問

4. 繼承

go語言其實也是提供了繼承的,只是採用的是組合的寫法,比如以下例子:

// 定義父類
type Animal struct {
    name string
    age int
}

// 父類方法
func (a *Animal) Say() {
    fmt.Println("animal say...")
}

// 定義子類繼承父類
type Dog struct {
    Animal
    weight int
}

func main() {
    d := &Dog{}
    
    d.Say()
    
    d.name = "旺財"
    
    fmt.Println(d) 
}

輸出:

animal say...
&{{旺財 0} 0}

沒有初始化值的變數會預設為對應型別的零值。

5. 多型

在理解go語言的多型之前,得先了解go語言的介面型別。

先來了解下其他語言的介面,在java中,對於介面的實現是必須在實現類中宣告要實現的介面的,如果要實現一個介面,需要像下面程式碼這樣編寫程式碼:

// 定義一個介面類
public interface Person {
    // 介面方法
    public void say();
}

// 定義實現類,需要使用關鍵字implements顯式的說明實現哪一個介面
class Teacher implements Person {
    public void say() {
        system.out.println("Hello 我是老師")
    }
}

而在go語言中,一個類只要實現了介面要求的所有函式,就可以說這個類實現了這個介面,當然go中介面使用的關鍵字還是interface

比如:

有一個File類,並且該類有四個方法,Read(),Write(),Seek(),Close()

type File struct {
    // ...
}
func (f *File) Read(buf []byte) (n int, err error)
func (f *File) Write(buf []byte) (n int, err error)
func (f *File) Seek(off int64, whence int) (pos int64, err error)
func (f *File) Close() error

然後有以下一些介面:

type IFile interface {
    Read(buf []byte) (n int, err error)
	Write(buf []byte) (n int, err error)
	Seek(off int64, whence int) (pos int64, err error)
	Close() error
}

type IReader interface {
    Read(buf []byte) (n int, err error)
}

type IWriter interface {
    Write(buf []byte) (n int, err error)
}

type ICloser interface {
    Close() error
}

程式碼中可以看出,File類並沒有明確表示從這些介面中繼承,甚至對於File類來說都不知道有這些介面的存在,但是在go裡,認為File類實現了這些介面。

因此可以這樣子進行賦值:

var file1 IFile = new(File)
var file2 IReader = new(File)
var file3 IWriter = new(File)
var file4 ICloser = new(File)

實質上,這樣子不就是多型麼!

介面的賦值:

go語言中介面賦值分為以下兩種情況:

  • 將物件例項賦值給介面

    這種情況要求物件例項實現了介面的所有方法,就比如上面的例子:var file1 IFile = new(File)

  • 將一個介面賦值給另一個介面

    在go語言中,只要兩個介面有相同的方法列表(次序不要求),那麼它們就是等同的,可以相互賦值。

    介面的賦值也不要求必須等價,如果介面A的方法列表是介面B的方法列表的子集,那麼介面B可以賦值給介面A,而介面A無法賦值給介面B,因為介面B中並沒有介面A中的其他方法,如果賦值給介面A了,當介面A呼叫一個存在於介面A而介面B不存在的方法,那就找不到了

    比如:

    假設有一個Writer介面和ReadWriter介面,實體類還是上面的File類

    type Writer interface {
        Write(buf []byte) (n int, err error)
    }
    
    type ReadWriter interface {
        Read(buf []byte) (n int, err error)
        Write(buf []byte) (n int, err error)
    }
    

    可以將ReadWriter介面的例項賦值給Writer介面:

    var file ReadWriter = new(File)
    // 介面ReadWriter 賦值給 介面Writer
    var file1 Writer = file
    // 這樣子是可以的,這樣file1是Writer介面的例項,只有一個Write方法可以呼叫是正常的
    

    但是反過來就不行了:

    var file Writer = new(File)
    // 介面ReadWriter 賦值給 介面Writer
    var file1 ReadWriter = file
    // 這樣子是不可以的,這樣file1是ReadWriter介面的例項,當file1呼叫read方法時候,並沒有這個方法,因為他實質是Writer介面型別
    

介面查詢

介面查詢可以檢查介面所指向的物件例項是否實現了某個介面,從而進行介面轉換,比如:

var file Writer = new(File)
if file1, ok := file.(ReadWriter); ok {
    ...
}

這裡是Writer介面所指向的物件例項是File類,是實現了ReadWriter的,所以這裡ok會為true,file1是ReadWriter介面的例項,所以相當於是從Writer介面轉為了ReadWriter介面了。

萬能型別

在Go語言中,有這麼一種空介面,原始碼裡是這樣的:type any = interface{},是一個空介面,根據之前對介面實現的理解,空接口裡沒有任何方法,那麼就可以認為所有的型別其實都是實現了這個介面的,因此這個interface{}可以指向任何物件,稱為Any型別,也叫萬能型別。

var a interface{} = new(int)
var b interface{} = new(string)
var c interface{} = struct{X int}{1}
a = 10
b = "hello"
fmt.Println(a, b, c) // 輸出:10 hello {1}

任何物件例項都實現了interface{},就類似於Java中的Object類一樣,那我們就可以用interface{}型別引用任意的資料型別了,像上面的程式碼那樣,這用在函式中傳參就很有用了!

型別查詢(型別斷言)

基於Go語言所有的物件例項都實現了空介面interface{}這個前提,那我們便可以直接了當的詢問介面指向的物件例項的型別:xxx.(type)

func test(arg interface{}) {
	switch arg.(type) {
	case int:
		fmt.Println("int type")
	case string:
		fmt.Println("string type")
	default:
		fmt.Println("unknown type")
	}
}

func main() {
    var v1 interface{} = "hello"
	var v2 int = 100
	v3 := struct{ X int }{1}

	test(v1)
	test(v2)
	test(v3)
}

12. 學習資料

《Go語言程式設計》

B站視訊:8小時轉職Golang工程師(如果你想低成本學習Go語言)