1. 程式人生 > >Go語言5-結構體

Go語言5-結構體

結構體

Go中的結構體(就相當於其它語言裡的class):

  • 用來定義複雜的資料結構
  • 可以包含多個欄位(屬性)
  • 結構體型別可以定義方法,注意和函式的區分
  • 結構體是值型別
  • 結構體可以巢狀
  • Go語言沒有class型別,只有struct型別

定義結構體

struct 宣告:

type (識別符號) struct {
    field1 type
    field2 type
}

例子:

type Student struct {
    Name string
    Age int
    Score int
}

結構體中欄位的訪問,和其他語言一樣,使用點:

package main

import "fmt"

type Student struct {
    Name string
    Age int
    Score int
}

func main() {
    var stu Student
    stu.Name = "Adam"
    stu.Age = 18
    stu.Score = 90
    fmt.Println(stu)
    fmt.Println(stu.Name)
    fmt.Println(stu.Age)
    fmt.Println(stu.Score)
}

struct 定義的3種形式:

var stu Student
var stu *Student = new (Student)
var stu *Student = &Student{}

後兩種返回的都是指向結構體的指標,所以需要再跟個等號分配記憶體空間。並且,有些場景應該是需要用指標的結構體會更加方便。
強調一下, struct 是值型別。這裡要用new來建立值型別。不是make,make是用來建立 map 、slice、channel 的。
結構體的訪問形式如下:

stu.Name
(*stu).Name

用上面兩種形式訪問都是可以的,但是定義的時候返回指標的話,標準做法還是應該用指標來訪問的,不過Go做了處理,可以簡化,直接用第一種就夠了,但是要知道呼叫的本質。

struct 的記憶體佈局

結構體是值型別,裡面所有的欄位在記憶體裡是連續的:

package main

import "fmt"

type Student struct {
    Name string
    Age int
    Score int
}

func main() {
    var stu Student
    stu.Name = "Adam"
    stu.Age = 18
    stu.Score = 90
    fmt.Println(stu)
    fmt.Printf("%p\n", &stu)
    fmt.Printf("%p\n", &stu.Name)
    fmt.Printf("%p\n", &stu.Age)
    fmt.Printf("%p\n", &stu.Score)
}

/* 執行結果
PS H:\Go\src\go_dev\day5\struct\attribute> go run main.go
{Adam 18 90}
0xc04204a3a0
0xc04204a3a0
0xc04204a3b0
0xc04204a3b8
PS H:\Go\src\go_dev\day5\struct\attribute>
*/

struct 初始化

package main

import "fmt"

type Student struct {
    Name string
    Age int
    Score int
}

func main(){
    var stu1 Student
    stu1.Name = "Adam"
    stu1.Age = 16
    stu1.Score = 90

    var stu2 Student = Student{
        Name: "Bob",
        Age: 15,
        Score: 85,
    }

    var stu3 *Student = &Student{
        Name: "Cara",
        Age: 18,
        Score: 80,
    }

    fmt.Println(stu1)
    fmt.Println(stu2)
    fmt.Println(&stu2)
    fmt.Println(stu3)
    fmt.Println(*stu3)
}

/* 執行結果
PS H:\Go\src\go_dev\day5\struct\init> go run .\main.go
{Adam 16 90}
{Bob 15 85}
&{Bob 15 85}
&{Cara 18 80}
{Cara 18 80}
PS H:\Go\src\go_dev\day5\struct\init>
*/

也可以在大括號裡按位置傳引數進行初始化和定義:

type Student struct {
    Name string
    Age int
    Score int
}

var s1 Student
s1 = Student {"stu1", 18, 90}
var s2 Student = Student{"stu2", 20, 80}

資料結構

用結構體定義資料型別

連結串列

每個節點包含下一個節點的地址,這樣就把所有的節點串起來了。通常把連結串列中的第一個節點叫做連結串列頭。

type Link struct {
    Name string
    Next *Link
}

下面有頭插法和尾插法建立連結串列,還有遍歷連結串列的方法:

package main

import "fmt"

type Student struct {
    Name string
    next *Student
}

// 遍歷連結串列的方法
func trans(p *Student) {
    for p != nil {
        fmt.Println(*p)
        p = p.next
    }
}

// 頭插法,從左邊插入
// 每個新加入的元素都插入到頭部元素的後面,這樣的好處是頭部元素的地址不變
func CreateLinkListLeft(p *Student) {
    var head = p
    for i := 0; i < 10; i++ {
        p := Student{
            Name: fmt.Sprintf("stuL%d", i),
        }
        p.next = head.next
        head.next = &p
    }
}

// 尾插法,從右邊加入
func CreateLinkListRight(p *Student) {
    var tail = p
    for i := 0; i < 10; i++ {
        p := Student{
            Name: fmt.Sprintf("stuR%d", i),
        }
        tail.next = &p
        tail = &p
    }
}

func main() {
    var headL Student
    fmt.Println("頭插法")
    CreateLinkListLeft(&headL)  // 結構體是值型別,要改變裡面的值,就是傳指標
    trans(&headL)

    var headR Student
    fmt.Println("尾插法")
    CreateLinkListRight(&headR)
    trans(&headR)
}

還有雙鏈表,詳細就不展開了:

type Link struct {
    Name string
    Next *Link
    Prev *Link
}

二叉樹

每個節點都有2個指標,分別用來指向左子樹和右子樹:

type binaryTree  struct {
    Name string
    left *binaryTree
    right *binaryTree
}

這裡只給一個深度優先的遍歷方法:

// 遍歷二叉樹,深度優先
func trans(root *Student) {
    if root == nil {
        return
    }
    // 前序遍歷
    fmt.Println(root)
    trans(root.left)
    trans(root.right)
}

最後3句的相對位置,主要是列印的方法的位置不同又有3種不同的叫法。上面這個是前序遍歷。如果列印放中間就是中序遍歷。如果列印放最後,就是後序遍歷。
廣度優先的遍歷方法,暫時能力還不夠。另外如果要驗證上面的遍歷方法,也只能用笨辦法來建立二叉樹。

結構體進階

看下結構體裡的一些高階用法

定義別名

可以給結構體取別名:

type Student struct {
    Name string
}

type Stu Student  // 取個別名

下面的程式碼,給原生的int型別取了個別名,也是可以像int一樣使用的:

package main

import "fmt"

type integer int

func main() {
    var i = integer = 100
        fmt.Println(i)
}

但是定義了別名的型別和原來的型別被系統認為不是同一個型別,不能直接賦值。但是是可以強轉型別的:

type integer int

func main() {
    var i integer = 100
        var j int
        // j = i  // 不同的型別不能賦值
        j = int(i)  // 賦值需要強轉型別
}

上面都是用原生的 int 型別演示的,自定義的結構體也是一樣的。

工廠模式(建構函式)

golang 中的 struct 不像其他語言裡的 class 有建構函式。struct 沒有建構函式,一般可以使用工廠模式來解決這個問題:

// go_dev/day5/struct/new/model/model.go
package model

type student struct {
    Name string
    Age int
}

func NewStudent(name string, age int) *student {
    // 這裡可以補充其他建構函式裡的程式碼
    return &student{
        Name: name,
        Age: age,
    }
}

// go_dev/day5/struct/new/main/main.go
package main

import (
    "../model"
    "fmt"
)

func main() {
    s := model.NewStudent("Adam", 20)
    fmt.Println(*s)
}

struct中的tag

可以為 struct 中的每個欄位,寫上一個tag。這個 tag 可以通過反射的機制獲取到。
為欄位加說明

type student struct {
    Name string  "This is name field"
    Age int  "This is age field"
}

json序列化
最常用的場景就是 json 序列化和反序列化。先看一下序列化的用法:

package main

import (
    "fmt"
    "encoding/json"
)

type Stu1 struct{
    name string
    age int
    score int
}

type Stu2 struct {
    Name string
    Age int
    score int  // 這個還是小寫,所以還是會有問題
}

func main() {
    var s1 Stu1 = Stu1 {"Adam", 16, 80}
    var s2 Stu2 = Stu2 {"Bob", 17, 90}
    var data []byte
    var err error
    data, err = json.Marshal(s1)
    if err != nil {
        fmt.Println("JSON err:", err)
    } else {
        fmt.Println(string(data))  // 型別是 []byte 轉成 string 輸出
    }
    data, err = json.Marshal(s2)
    if err != nil {
        fmt.Println("JSON err:", err)
    } else {
        fmt.Println(string(data))
    }
}

/* 執行結果
PS H:\Go\src\go_dev\day5\struct\json> go run main.go
{}
{"Name":"Bob","Age":17}
PS H:\Go\src\go_dev\day5\struct\json>
*/

結構體中,小寫的欄位外部是訪問不了的,所以第一個輸出是空的。而第二個結構體中只有首字母大寫的欄位才做了序列化。
所以一般結構體裡的欄位名都是首字母大寫的,這樣外部才能訪問到。不過這樣的話,序列化之後的變數名也是首字母大寫的。而json是可以實現跨語言傳遞資料的,但是在其他語言裡,都是習慣變數小寫的。這樣go裡json序列化出來的資料在別的語言裡看就很奇怪。
在go的json包裡,通過tag幫我們做了優化。會去讀取欄位的tag,去裡面找到json這個key,把對應的值,作為欄位的別名。具體做法如下:

package main

import (
    "fmt"
    "encoding/json"
)

type Student struct{
    Name string `json:"name"`
    Age int `json:"age"`
    Score int `json:"score"`
}

func main() {
    var stu Student = Student{"Cara", 16, 95}
    data, err := json.Marshal(stu)
    if err != nil {
        fmt.Println("JSON err:", err)
        return
    }
    fmt.Println(string(data))  // 型別是 []byte 轉成 string 輸出
}

/* 執行結果
PS H:\Go\src\go_dev\day5\struct\json_tag> go run main.go
{"name":"Cara","age":16,"score":95}
PS H:\Go\src\go_dev\day5\struct\json_tag>
*/

反引號,作用和雙引號一樣,不過內部不做轉義。

匿名欄位

結構體力的欄位可以沒有名字,即匿名欄位。

type Car struct {
    Name string
    Age int
}

type Train struct {
    Car  // 這個Car也是型別,上面定義的。這裡沒有名字
    Start time.TIme
    int  // 這個欄位也沒有名字,即匿名欄位
}

訪問匿名欄位
可以直接通過匿名欄位的型別來訪問,所以匿名欄位的型別不能重複:

var t Train
t.Car.Name = "beemer"
t.Car.Age = 3
t.int = 100

對於結構體型別,還可以在簡化,結構體的名字可以不寫,下面的賦值和上面的效果一樣:

var t Train
t.Name = "beemer"
t.Age = 3

匿名欄位衝突處理

type Car struct {
    Name string
    Age int
}

type Train struct {
    Car
    Start time.TIme
    Age int  // 這個欄位也叫 Age
}

var t Train
t.Age  // 這個Age是Train裡的Age
t.Car.Age  // Car裡的Age現在只能把型別名加上了

通過匿名欄位實現繼承
匿名欄位在需要有繼承的的場景下很好用:

type Animal struct {
    Color string
    Age int
    Weight int
    Type string
}

type Dog Struct {
    Animal 
    Name string
    Weight float32
}

定義了一個 Animal 動物類,裡面有很多屬性。再定義一個 Dog 狗的類,也屬於動物,需要繼承動物的屬性。這裡用匿名欄位就方便的繼承過來了。並且有些欄位還可以再重新定義覆蓋原先的,比如例子裡的 Weight 。這樣 Dog 就有 Animal 的所有的欄位,並且 Dog 還能新增自己的欄位,也可以利用衝突覆蓋父類裡的欄位。

方法(method)

Golang 中的方法是作用在特定型別的變數上的。因此自定義型別也可以有方法,而不僅僅是 struct 。

定義方法

func (變數名 方法所屬的型別) 方法名 (引數列表) (返回值列表) {}
方法和函式的區別就是在func關鍵字後面多了 (變數名 方法所屬的型別) 。這個也別稱為方法的接收器(receiver)。這個是宣告這個方法是屬於哪個型別,這裡的型別也包括 struct。
Golang裡的接收器沒有 this 或者 self 這樣的特殊關鍵字,所以名字可以任意取的。一般而言,出於對一致性和簡短的需要,我們使用型別的首字母。類比 self ,也就知道這個接收器的變數在方法定義的程式碼塊裡就是代指當前型別的例項。

package main

import "fmt"

type Student struct {
    Name string
    Age int
}

func (s *Student) growup () {
    s.Age++
} 

func (s *Student) rename (newName string) {
    s.Name = newName
} 

func main() {
    var stu Student = Student{"Adam", 17}
    fmt.Println(stu)
    stu.growup()
    fmt.Println(stu)
    stu.rename("Bob")
    fmt.Println(stu)
}

/* 執行結果
PS H:\Go\src\go_dev\day5\method\beginning> go run main.go
{Adam 17}
{Adam 18}
{Bob 18}
PS H:\Go\src\go_dev\day5\method\beginning>
*/

上面的2個方法裡的接收器型別加了星號。如果不用指標的話,傳入的是物件的副本,方法改變是副本的值,不會改變原來的物件。
另外上面呼叫方法的用法也已經簡寫了,實際是通過結構體的地址呼叫的 (&stu).growup()

繼承

匿名欄位就是繼承的用法。不但可以繼承欄位,方法也是繼承的:

package main

import "fmt"

type Animal struct {
    Type string
}

func (a Animal) hello() {
    fmt.Println(a.Type, "Woo~~")
}

type Dog struct {
    Animal
    Name string
}

func main() {
    var a1 Animal = Animal{"Tiger"}
    var d1 Dog
    d1.Type = "Labrador"
    d1.Name = "Seven"
    a1.hello()
    d1.hello()  // Dog 也能呼叫 Animal 的方法
}

多繼承
一個 struct 裡用了多個匿名結構體,那麼這個結構體就可以直接訪問多個匿名結構體的方法,從而實現了多繼承。

組合

如果一個 struct 嵌套了另一個匿名 struct,這個結果可以直接訪問匿名結構的方法,從而實現了繼承。
如果一個 struct 嵌套了另一個有名 struct,這個模式就叫組合:

package main

import "fmt"

type School struct {
    Name string
    City string
}

type Class struct {
    s School
    Name string
}

func main() {
    var s1 School = School{"SHHS", "DC"}
    var c1 Class
    c1.s = s1
    c1.Name = "Class One"
    fmt.Println(c1)
    fmt.Println(c1.s.Name)
}

繼承與組合的區別:

  • 繼承:建立了派生類與基類之間的關係,它是一種“是”的關係,比如:狗是動物。當類之間有很多相同的功能,提取這些共同的功能做成基類,用繼承比較好。
  • 組合:建立了類與組合類之間的關係,它是一種“有”的關係,比如老師有生日,老師有課程,老師有學生

實現 String()

如果一個變數實現了 String() 方法,那麼 fmt.Println 預設會呼叫這個變數的 String() 進行輸出。

package main

import "fmt"

type Animal struct {
    Type string
    Weight int
}

type Dog struct {
    Animal
    Name string
}

func (d Dog) String () string{
    return d.Name + ": " + d.Type
}

func main(){
    var a1 Animal = Animal{"Tiger", 230}
    var d1 Dog = Dog{Animal{"Labrador", 100}, "Seven"}
    fmt.Println(a1)
    fmt.Println(d1)
}

/* 執行結果
PS H:\Go\src\go_dev\day5\method\string_method> go run main.go
{Tiger 230}
Seven: Labrador
PS H:\Go\src\go_dev\day5\method\string_method>
*/

注意傳值還是傳指標,例子裡定義的時候沒有星號,是傳值的,列印的時候也是傳值就有想過。列印的使用用 fmt.Println(&d1) 也是一樣的。但是如果定義的時候用了星號,就是傳指標,列印的時候就必須加上&把地址傳進去才有效果。否則就是按照原生的方法打印出來。

介面(多型)

這是go語言多型的實現方式
Interface 型別可以定義一組方法,但是這些不需要實現,並且 interface 不能包含任何變數。

定義介面

定義介面使用 interface 關鍵字。然後只需要再裡面定義一個或者多個方法就好,不需要實現:

type 介面名 interface {
    方法名1(引數列表)
    方法名2(引數列表) [返回值]
    方法名3(引數列表) [返回值]
}

interface 型別預設是一個指標,預設值是空 nil 。

介面實現

Golang 中的介面,不需要顯式的實現,只要一個變數,含有介面型別中的所有方法,那麼這個變數就實現了這個介面。因此,golang 中沒有 implement 型別的關鍵字
如果一個變數含有了多個 interface 型別的方法,那麼這個變數就實現了多個介面。
下面是一個介面實現的示例:

package main

import "fmt"

// 定義一個介面
type AnimalInterface interface {
    Sleep()  // 定義一個方法
    GetAge() int  // 再定義一個有返回值的方法
}

// 定義一個類
type Animal struct {
    Type string
    Age int
}  // 接下來要實現接口裡的方法

// 實現介面的一個方法
func (a Animal) Sleep() {
    fmt.Printf("%s need sleep\n", a.Type)
}

// 實現了介面的另一個方法
func (a Animal) GetAge() int {
    return a.Age
}

// 又定義了一個類,是上面的子類
type Pet struct {
    Animal
    Name string
}

// 重構了一個方法
func (p Pet) sleep() {
    fmt.Printf("%s need sleed\n", p.Name)
}  // 有繼承,所以Age方法會繼承父類的

func main() {
    var a1 Animal = Animal{"Dog", 5}  // 建立一個例項
    var aif AnimalInterface  // 建立一個介面
    aif = a1  // 因為類裡實現了介面的方法,所以可以賦值給介面
    aif.Sleep()  // 可以用介面呼叫
    a1.Sleep()  // 使用結構體呼叫也是一樣的效果,這就是多型

    var p1 Pet = Pet{Animal{"Labrador", 4}, "Seven"}
    aif = p1
    aif.Sleep()
    fmt.Println(aif.GetAge())
}

多型

一種食物的多種形態,都可以按照統一的介面進行操作。
多型,簡單點說就是:"一個介面,多種實現"。比如 len(),你給len傳字串就返回字串的長度,傳切片就返回切片長度。

package main

import "fmt"

func main() {
    var s1 string = "abcdefg"
    fmt.Println(len(s1))
    var l1 []int =  []int{1, 2, 3, 4}
    fmt.Println(len(l1))
}

參照上面的,自己寫的方法也可以接收介面作為引數,對不同的型別對應多種實現:

package main

import "fmt"

type Msg interface {
    Print()
}

type Message struct {
    msg string
}

func (m Message) Print() {
    fmt.Println("Message:", m.msg)
}

type Information struct {
    msg string
    level int
}

func (i Information) Print() {
    fmt.Println("Information:", i.level, i.msg)
}

func interfaceUse(m Msg) {
    m.Print()
}

func main() {
    message := Message{"Hello"}  // 定義一個結構體
    information := Information{"Hi", 2}  // 定義另外一個型別的結構體
    // 這裡並不需要 var 介面,以及賦值
    interfaceUse(message)  // 引數不看型別了,而看你是否滿足介面
    interfaceUse(information)  // 雖然這裡的引數和上面不是同一個型別,但是這裡對引數的要求是介面
}

這裡並不需要顯示的 var 介面以及賦值。

介面巢狀

一個介面可以巢狀另外的介面:

type ReadWrite interface {
    Read(b Buffer) bool
    Write(b Buffer) bool
}

type Lock interface {
    Lock()
    Unlock
}

type File interface {
    ReadWrite
    Lock
    Close
}

巢狀的用法類似結構體的繼承。

型別斷言

型別斷言,由於介面是一般型別,不知道具體的型別。如果要轉成具體型別,可以採用一下方法進行轉換:

package main

import "fmt"

func main() {
    i := 10
    var j interface{}
    j = i  // 這裡通過將變數作為一個interface{}的方法來進行下面的型別斷言
    res := j.(int)  // 在這裡只要呼叫的不是interface{}就不行,所以上面要賦值一下
    fmt.Println(res)
    fmt.Println(interface{}(j).(int))  // 上面幾行可以這麼寫
}

上面是不帶檢查的,如果型別轉換不成功,會報錯。下面是帶檢查的型別斷言:

package main

import "fmt"

type Num struct {
    n int
}

func main() {
    var i Num = Num{1}
    var j interface{}
    j = i  // 這裡通過將變數作為一個interface{}的方法來進行下面的型別斷言
    res, ok := j.(int)  // 在這裡只要呼叫的不是interface{}就不行,所以上面要賦值一下
    fmt.Println(res, ok)
    res, ok = interface{}(j).(int)  // 上面幾行可以這麼寫
    fmt.Println(res, ok)
}

面向物件

golang 中並沒有明確的面向物件的說法,可以將 struct 類比作其它語言中的 class。

constructor 建構函式
通過結構體的工廠模式返回例項來實現

Encapsulation 封裝
通過自動的大小寫控制可見

Inheritance 繼承
結構體巢狀匿名結構體

Composition 組合
結構體巢狀有名結構體

Polymorphism 多型
通過介面實現

課後作業

實現一個圖書管理系統,具有以下功能:

  • 書籍錄入功能:書籍資訊包括書名、副本數、作者、出版日期
  • 書籍查詢功能:按照書名、作者、出版日期等條件檢索
  • 學生資訊管理功能:管理每個學生的姓名、年級、×××、性別、借了什麼書等資訊
  • 借書功能:學生可以查詢想要的書籍,進行借出
  • 書籍管理功能:可以看到每種書被哪些人借出了