Go語言基礎(2)

Go語言基礎(2)

Go語言中支持函數、匿名函數和閉包

Go語言中定義函數

定義函數使用func關鍵字,語法如下:

func 函數名(參數)(返回值){
  函數體
}

說明:

​ 1) 函數名:由字母、數字、下劃線組成。但函數名的第一個字母不能是數字。在同一個包內,函數名也稱不能重名(包的概念詳見後文)。

​ 2) 參數:參數由參數變量和參數變量的類型組成,多個參數之間使用,分隔。

​ 3) 返回值:返回值由返回值變量和其變量類型組成,也可以只寫返回值的類型,多個返回值必須用()包裹,並用,分隔。

​ 4) 函數體:實現指定功能的代碼塊。

例:

func intSum(x int, y int) int {
    return x+y
}

func main() {
    fmt.Println(intSum(3,4))
}

函數的參數和返回值都屬於可選項

函數調用

可以通過函數名()的方式調用函數

可變參數

可變參數是指函數的參數數量不固定。Go語言中的可變參數通過在參數名後加...來標識

func intSum2(x ...int) int {
    fmt.Printf("x type is: %T\n", x) // x type is: []int
    sum := 0
    for _, item := range x {
        sum = sum + item
    }
    return sum
}

多返回值:

func calc(x,y int)(int,int) {
    return x+y, x-y
}

一個函數返回值類型爲slice時,nil可以看做是一個有效的slice,沒必要顯示返回一個長度爲0的切片。

func demo() []int {
    return nil
}

變量作用域

全局變量是定義在函數外部的變量,它在程序整個運行週期內都有效。

局部變量又分爲兩種: 1)函數內定義的變量無法在該函數外使用;

2) 語句塊定義的變量在語句塊外無法使用,通常會在if條件判斷、for循環、switch語句上使用這種定義變量的方式

函數類型的變量

使用type關鍵字來定義一個函數類型

type calculation func(x,y int) int
func add(x,y int) int {
    return x + y

}

func sub(x,y int) int {
    return x - y

}

func main() {
    var c1 calculation
    c1 = add
    fmt.Println(c1(3,4))
    fmt.Printf("type of c:%T\n", c1)    //type of c:main.calculation
}

匿名函數

函數當然還可以作爲返回值,但是在Go語言中函數內部不能再像之前那樣定義函數了,只能定義匿名函數。匿名函數就是沒有函數名的函數,匿名函數的定義格式如下:

func(參數)(返回值){
  函數體
}

閉包

閉包: 外層函數返回內層函數,內層函數調用外層函數的變量;

func f1(x int) func(int) int{
    return func (y int) int {
        return x*y
    }
}

func main() {
    a1 :=f1(3)
    fmt.Println(a1(4))  // 輸出12
}

高價函數

func add(x,y int) int {
    return x + y
}

func main() {
    fmt.Println(f2(5,2, add)) // 輸出7 
}

defer語句

Go語言中的defer語句會將其後面跟隨的語句進行延遲處理。在defer歸屬的函數即將返回時,將延遲處理的語句按defer定義的逆序進行執行,也就是說,先被defer的語句最後被執行,最後被defer的語句,最先被執行

在Go語言的函數中return語句在底層並不是原子操作,它分爲給返回值賦值和RET指令兩步。而defer語句執行的時機就在返回值賦值操作後,RET指令執行前。具體如下圖所示

Go語言基礎(2)

內置函數

內置函數 介紹
close 主要用來關閉channel
len 用來求長度,比如string、array、slice、map、channel
new 用來分配內存,主要用來分配值類型,比如int、struct。返回的是指針
make 用來分配內存,主要用來分配引用類型,比如chan、map、slice
append 用來追加元素到數組、slice中
panic和recover 用來做錯誤處理

panic/recover

Go語言中目前(Go1.12)是沒有異常機制,但是使用panic/recover模式來處理錯誤。 panic可以在任何地方引發,但recover只有在defer調用的函數中有效。

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

func t2() {
    defer func() {
        err := recover()
        if err != nil{
            fmt.Println("recover in func t2")
        }
    }()
    panic("panic in func t2")
}

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

func main() {
    t1()
    t2()
    t3()
}

注意:

  1. recover()必須搭配defer使用。
  2. defer一定要在可能引發panic的語句之前定義

結構體

自定義類型

自定義類型是定義了一個全新的類型。可以基於內置的基本類型定義,也可以通過struct定義。例如:

//將MyInt定義爲int類型
type MyInt int

通過type關鍵字的定義,MyInt就是一種新的類型,它具有int的特性。

類型別名

類型別名規定:TypeAlias只是Type的別名,本質上TypeAlias與Type是同一個類型。就像兒時的乳名,上學後用學名,英語老師又會給起英文名,但這些名字都指的是他本人。

type TypeAlias = Type

rune和byte就是類型別名,內置的定義如下:

type byte = uint8
type rune = int32

類型別名與類型定義表面上看只有一個等號的差異,區別如下:

//類型定義
type NewInt int

//類型別名
type MyInt = int

func main() {
    var a NewInt
    var b MyInt

    fmt.Printf("type of a:%T\n", a) //type of a:main.NewInt
    fmt.Printf("type of b:%T\n", b) //type of b:int
}

結構體

Go語言提供了一種自定義數據類型,可以封裝多個基本數據類型,這種數據類型叫結構體,英文名稱struct。

語法如下:

type 類型名 struct {
  字段名 字段類型
  字段名 字段類型
  …
}

說明:

1) 類型名:標識自定義結構體的名稱,在同一個包內不能重複。

2) 字段名:表示結構體字段名。結構體中的字段名必須唯一。

3) 字段類型:表示結構體字段的具體類型。

結構體實例化

只有當結構體實例化時,纔會真正地分配內存。也就是必須實例化後才能使用結構體的字段

結構體本身也是一種類型,可以像聲明內置類型一樣使用var關鍵字聲明結構體類型。

var 結構體實例 結構體類型

Go語言支持匿名結構體,也支持指針類型結構體

例:

type person struct {
    name string
    age int
    marrired bool
}

func main() {
    var p01 person
    p01 = person{
        name: "zhsansan",
        age: 18,
        marrired: false,
    }
    fmt.Println(p01)    //{zhsansan 18 false}
    var s01 struct{name string; score int}  //匿名結構體
    s01.name = "tom"
    s01.score = 80
  fmt.Println(s01)  //{tom 80}

    p02 := new(person)  //通過new關鍵字對結構體進行實例化,得到的是結構體的地址。p02的類型是一個結構指針
    p02.name = "eric"
    p02.age = 25
    p02.marrired = true
    fmt.Println(*p02)   //{eric 25 true}

    p03 := &person{}    //使用&對結構體進行取地址操作相當於對該結構體類型進行了一次new實例化操作
    p03.name = "robin"
    p03.age = 22
    p03.marrired = false
    fmt.Println(*p03) //{robin 22 false}

    p04 := &person{name: "hallen", age: 20, marrired: false}
    fmt.Println(*p04)   //{hallen 20 false}
}

實例化之後的結構體佔用一塊連續的內存。空結構體是不佔用空間的。

構造函數

Go語言的結構體沒有內置的構造函數,需要自定義構造函數。如果結構體比較複雜的話,值拷貝性能開銷會比較大,所以該構造函數返回的是結構體指針類型。

Go語言中的方法(Method)是一種作用於特定類型變量的函數。這種特定類型變量叫做接收者(Receiver)。

方法的定義格式如下:

func (接收者變量 接收者類型) 方法名(參數列表) (返回參數) {
  函數體
}

例:

func newPerson(name string, age int, married bool) *person {
    return &person{name: name, age: age, marrired: married}
}

func (p person)Dream(){
    fmt.Printf("%s's dream is rich\n", p.name)
}

func main(){
    p05 := newPerson("natasha", 24, false)
    fmt.Println(*p05)   //{natasha 24 false}
    p05.Dream() //natasha's dream is rich
}

方法與函數的區別是,函數不屬於任何類型,方法屬於特定的類型。

指針類型的接收者

指針類型的接收者由一個結構體的指針組成,由於指針的特性,調用方法時修改接收者指針的任意成員變量,在方法結束後,修改都是有效的。

當方法作用於值類型接收者時,Go語言會在代碼運行時將接收者的值複製一份。在值類型接收者的方法中可以獲取接收者的成員值,但修改操作只是針對副本,無法修改接收者變量本身。

什麼時候應該使用指針類型接收者

1)需要修改接收者中的值

2)接收者是拷貝代價比較大的大對象

3)保證一致性,如果有某個方法使用了指針接收者,那麼其他的方法也應該使用指針接收者

例:

func (p *person)setAge(age int){
    p.age = age
}

func main(){
    p05.setAge(21)
    fmt.Println(*p05)       //{natasha 21 false}
}

爲內置類型添加方法

例:

type MyInt int

func (m MyInt)SayTest(){
    fmt.Println("hello world")
}

func main() {
    var m1 MyInt
    m1.SayTest()        // hello world
    m1 = 100
    fmt.Printf("%v %T\n", m1, m1)   //100 main.MyInt
}

結構的嵌套

一個結構體中可以嵌套包含另一個結構體或結構體指針,如下所示:

type Address struct {
    Province string
    City string
}

type User struct {
    Name string
    Gender string
    Address Address
}

type User2 struct {
    Name string
    Gender string
    Address     //匿名字段
}

func main() {
    user1 := User{
        Name: "Sam",
        Gender: "male",
        Address: Address{
            Province: "河北",
            City: "保定",
        },
    }
    fmt.Println(user1)  //{Sam man {河北 保定}}

    var user2 User2
    user2.Name = "Alex"
    user2.Gender = "male"
    user2.Address.Province = "遼寧"
    user2.City = "大連"
    fmt.Println(user2)  //{Alex male {遼寧 大連}}
}

結構體繼承

Go語言中使用結構體也可以實現結構體的繼承,如下所示:

type Animal struct {
    name string
}

func (a *Animal)move() {
    fmt.Println("會動了")
}

type Dog struct {
    Feet int
    *Animal     //通過嵌套匿名結構體實現繼承
}

func (d *Dog)wang() {
    fmt.Println("汪汪汪")
}

func main() {
    d1 := &Dog{
        Feet: 4,
        Animal: &Animal{    //注意嵌套的是結構體指針
            name: "hely",
        },
    }
    d1.move()
    d1.wang()
}

結構體中字段大寫開頭表示可公開訪問,小寫表示私有(僅在定義當前結構體的包中可訪問)

結構體標籤

Tag是結構體的元信息,可以在運行的時候通過反射的機制讀取出來。 Tag在結構體字段的後方定義,由一對反引號包裹起來,具體的格式如下:

key1:"value1" key2:"value2"

結構體tag由一個或多個鍵值對組成。鍵與值使用冒號分隔,值用雙引號括起來。同一個結構體字段可以設置多個鍵值對tag,不同的鍵值對之間使用空格分隔。

注意事項: 爲結構體編寫Tag時,必須嚴格遵守鍵值對的規則。結構體標籤的解析代碼的容錯能力很差,一旦格式寫錯,編譯和運行時都不會提示任何錯誤,通過反射也無法正確取值。例如不要在key和value之間添加空格。

包(package)

包(package)是多個Go源碼的集合,是一種高級的代碼複用方案,Go語言爲我們提供了很多內置包,如fmt、os、io等

可以根據自己的需要創建自己的包。一個包可以簡單理解爲一個存放.go文件的文件夾。 該文件夾下面的所有go文件都要在代碼的第一行添加如下代碼,聲明該文件歸屬的包。

package 包名

注意事項:

1) 一個文件夾下面直接包含的文件只能歸屬一個package,同樣一個package的文件不能在多個文件夾下。

2) 包名可以不和文件夾的名字一樣,包名不能包含 - 符號。

3) 包名爲main的包爲應用程序的入口包,這種包編譯後會得到一個可執行文件,而編譯不包含main包的源代碼則不會得到可執行文件。

在一個包中引用另外一個包裏的標識符(如變量、常量、類型、函數等)時,該標識符必須是對外可見的(public)。在Go語言中只需要將標識符的首字母大寫就可以讓標識符對外可見了。

結構體中的字段名和接口中的方法名如果首字母都是大寫,外部包可以訪問這些字段和方法。

要在代碼中引用其他包的內容,需要使用import關鍵字導入使用的包。具體語法如下:

import "包的路徑"

注意事項:

1) import導入語句通常放在文件開頭包聲明語句的下面。

2) 導入的包名需要使用雙引號包裹起來。

3) 包名是從$GOPATH/src/後開始計算的,使用/進行路徑分隔。

4) Go語言中禁止循環導入包。

在導入包名的時候,還可以爲導入的包設置別名。

import 別名 "包的路徑"

如果只希望導入包,而不使用包內部的數據時,可以使用匿名導入包。具體的格式如下:

import _ "包的路徑"

匿名導入的包與其他方式導入的包一樣都會被編譯到可執行文件中。

init()函數

在Go語言程序執行時導入包語句會自動觸發包內部init()函數的調用。需要注意的是: init()函數沒有參數也沒有返回值。 init()函數在程序運行時自動被調用執行,不能在代碼中主動調用它。

包初始化執行的順序是: 全局聲明--> init() ---> main()

init()函數執行順序

Go語言包會從main包開始檢查其導入的所有包,每個包中又可能導入了其他的包。Go編譯器由此構建出一個樹狀的包引用關係,再根據引用順序決定編譯順序,依次編譯這些包的代碼。

在運行時,被最後導入的包會最先初始化並調用其init()函數, 如下圖示:

Go語言基礎(2)

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