1. 程式人生 > >Go語言基礎之結構體(面向物件程式設計上)

Go語言基礎之結構體(面向物件程式設計上)

1 自定義型別和類型別名

1.1 自定義型別

Go語言中可以基於一些基本的資料型別,使用type關鍵字定義自定義型別的資料 。

自定義型別定義了一個全新的型別,該新型別具有基本資料型別的特性。自定義型別定義的方法如下:

type TypeName Type

//將 NewType定義為int 型別
type NewType int

NewType是一個新的型別,其具有int的特性。

1.2 類型別名

類型別名是Go1.9版本新增的新功能。利用類型別名的功能,可以給一些基本的資料型別定義一些讓讀者見名知意的名字,從而提高程式碼的可讀性。類型別名定義的方法如下:

type TypeAlias = Type

Go語言中的runebyte就是類型別名,它們的定義如下:

type byte = uint8
type rune = int32

1.3 自定義型別和類型別名的區別

自定義型別:自定義型別定義了一個全新的型別,其繼承了基本型別的所有特性,並且可以實現新型別的特有的一些方法。

類型別名:只存在程式碼編寫的過程,程式碼編譯以後根本不存在這個類型別名。其作用用來提高程式碼的可讀性。

如下程式碼,體現了二者的區別:

//自定義型別
type NewInt int

//類型別名
type MyInt = int

func main(){
    var a NewInt
    var b MyInt
    var c int
    // c = a  //?  可以使用強制型別轉換c = int(a)
    c = b  //c和b是同一型別
    
    fmt.Println("type of a:%T\n", a)  //type of a:main.NewInt
    fmt.Println("type of b:%T\n", b)  //type of b:int
}

2 結構體

在Go語言中可以使用基本資料型別表示一些事物的屬性,但是如果想表達一個事物的全部或部分屬性,比如說一個學生(學號、姓名、年齡、班級等),這時單一的基本資料型別就不能夠滿足需求。

Go語言中提供了一種自定義資料型別,可以將多個基本資料型別或引用型別封裝在一起,這種資料型別叫struct結構體。Go語言也是通過struct來實現面向物件的。

2.1 Golang語言面向物件程式設計說明

  • Golang也支援面向物件程式設計(OOP),但是和傳統的面向物件程式設計有區別,並不是純粹的面嚮物件語言。所以說Golang支援面向物件程式設計特性是比較準確的;
  • Golang沒有類(class),Go語言的結構體(struct)和其他程式語言的類(class)有同等的地位,可以理解Golang是基於struct
    來實現OOP特性的;
  • Golang面向物件程式設計非常簡潔,去掉了傳統OOP語言的繼承、方法過載、建構函式和解構函式、隱藏的this指標等;
  • Golang仍然有面向物件程式設計的繼承、封裝和多型的特性,只是實現的方法和其他OOP語言不一樣,比如繼承:Golang的繼承是通過匿名欄位來實現的;
  • Golang面向物件(OOP)很優雅,OOP本身就是語言型別系統(type system)的一部分,通過介面(interface)關聯,耦合性低,也非常靈活。Golang中面向介面程式設計是非常重要的特性。

2.2 快速入門

// 建立一個結構體型別的student
type student struct {
    name string
    age int
    gender string
    hobby []string
}

func main() {
    // 建立一個student例項
    var viktor = student{
        name:   "viktor",
        age:    24,
        gender: "男",
        hobby:  []string{"乒乓球", "羽毛球"},
    }

    fmt.Println(viktor)         //  {viktor 24 男 [乒乓球 羽毛球]}
    // 分別取出viktor例項中的每個欄位
    fmt.Println(viktor.name)    // viktor
    fmt.Println(viktor.age)     // 24
    fmt.Println(viktor.gender)  // 男
    fmt.Println(viktor.hobby)   // [乒乓球 羽毛球]
}

2.3 如何宣告結構體

基本語法:

type StructName struct {
    field1 type
    field2 type
}

示例,宣告一個學生的結構體Student

type Student struct {
    Name string
    Age int
    Score float32
}

2.4 欄位/屬性

struct中封裝了一些基本資料型別的變數,我們稱之為結構體欄位或者是該結構體的屬性。

欄位是結構體的一個組成部分,一般是基本資料型別、陣列,也可以是引用型別,甚至是struct(巢狀結構體)等。

注意事項和細節說明:

  • 欄位宣告語法同變數;

  • 在建立一個結構體變數後,如果沒有給欄位賦值,都對應一個零值(預設值):布林型別是false,數值是0,字串是"",陣列的預設值和它的元素型別相關,比如 score [3]int則為[0, 0, 0],指標、slice和map的零值都是nil,即還沒有分配空間。

type Person struct {
    Name string 
    Age int
    Scores [5]float64
    ptr *int
    slice []int
    map1 map[string]string
}

func main() {
    //定義結構體變數
    var p1 Person
    fmt.Println(p1)
    
    if p1.ptr == nil {
        fmt.Println("ok1")
    }
    
    if p1.slice == nil {
        fmt.Println("ok2")
    }
    
    if p1.map1 == nil {
        fmt.Println("ok3")
    }
    
    p1.slice = make([]int, 10)
    p1.slice[0] = 100
    
    p1.map1 = make(map[string]string)
    p1.map1["key1"] = "tom"
    fmt.Println(p1)
}
  • 不同結構體變數的欄位是獨立,互不影響,一個結構體變數欄位的更改,不影響另外一個,結構體是值型別。

2.5 結構體的例項化

可以這樣理解,宣告一個結構體類似創造一個模具,如果想要真正的描述一個事物,那麼就得使用這個模具來製造一個事物,這個製造事物的過程稱為 建立結構體變數或者結構體例項化。結構體的例項化有四種方式:

  • 方式1:直接宣告
type Person struct {
    Name string 
    Age int
}

func main() {
    //定義結構體變數
    var p1 Person
    fmt.Println(p1)
}
  • 方式2:{}
//使用值列表初始化
p2 := Person{"tom", 20}
fmt.Println(p2)
  • 方式3:使用new關鍵字

struct是值型別,那麼就可以使用new關鍵字定義一個結構體指標:

var p3 *Person = new(Person)
(*p3).Name = "smith"
p3.Name = "john"

(*p3).Age = 20
p3.Age = 30
fmt.Println(p3)
  • 方式4:使用&,定義一個結構體指標
//使用鍵值對初始化
var person *Person = &Person{
    Name : "tom", 
    Age : 19,
}

//也可以通過欄位訪問的形式進行賦值
(*p3).Name = "scott"
p3.Name = "scott~"

(*p3).Age = 20
p3.Age = 30
fmt.Println(p3)

說明:

  • 第三種和第四種方式返回的是結構體指標;
  • 在結構體初始化是,必須初始化結構體的所有欄位;初始值的填充順序必須與欄位在結構體中的宣告順序一致;值列表初始化方式和鍵值初始化方式不能混用;
  • 結構體指標訪問欄位的標準方式是:(*結構體指標).欄位名
  • 由於Go語言中的指標不支援偏移和運算,語句go編譯器底層對person.Name做了轉化(*person).Name

2.6 結構體使用注意事項和細節

  • 結構體的所有欄位在記憶體中是連續的;
type Point struct {
    x, y int
}

type Rect struct {
    leftUp, rightDown Point
}

type Rect2 struct {
    leftUp, rightDown *Point
}

func main() {
    r1 := Rect(Point{1,2}, Point{3,4})
    //r1有四個int,在記憶體中是連續分佈
    fmt.Printf("r1.leftUp.x 地址=%p r1.leftUp.y 地址=%p r1.rightDown.x 地址=%p r1.rightDown.y 地址=%p", &r1.leftUp.x, &r1.leftUp.y, &r1.rightDown.x, &r1.rightDown.y)
    
    //r2有兩個*Point型別,這兩個*Point型別的本身地址也是連續的,但是其指向的地址不一定是連續的
    r2 := Rect(&Point{10,20}, Point{30,40})
    
    fmt.Printf("r2.leftUp 本身地址=%p r2.rightDown 本身地址=%p", &r2.leftUp, &r2.rightDown)
    
    fmt.Printf("r2.leftUp 指向地址=%p r2.rightDown 指向地址=%p", r2.leftUp, r2.rightDown)
}
  • 結構體是使用者單獨定義的型別,和其他型別進行轉換時需要有完全相同的欄位(名字、個數和型別)
  • struct的每個欄位上,可以寫上一個tag,該tag可以通過反射機制獲取,常見的使用場景:序列化和反序列化。
type Monster struct {
    Name string `json:"name"`
    Age int `json:"age"`
    Skill string `json:"skill"`
}

func main() {
    //建立一個Monster例項
    monster := Monster{"蜘蛛精", 200, "吐絲"}
    
    //將monster序列化
    jsonStr, err := json.Marshal(monster)
    if err != nil {
        fmt.Println("json 處理錯誤", err)
    }
    fmt.Println("jsonStr", string(jsonStr))
}

2.7 面試題

下面程式碼的執行結果?

type student struct {
    name string
    age  int
}

func main() {
    m := make(map[string]*student)
    stus := []student{
        {name: "李四", age: 18},
        {name: "張三", age: 23},
        {name: "李明", age: 9000},
    }

    for _, stu := range stus {
        m[stu.name] = &stu
    }
    for k, v := range m {
        fmt.Println(k, "=>", v.name)
    }
}

3 方法

方法是什麼?在聲明瞭一個結構體後,比如說Person結構體,那麼這個人都有哪些功能,或者說都有什麼能力,這些功能或者能力就是一個結構體的方法。

Golang中的方法是作用在指定的資料型別上(即,和指定的資料型別繫結),因此自定義型別,都可以有方法,而不僅僅是struct。如下示例:

//MyInt 將int定義為自定義MyInt型別
type MyInt int

//SayHello 為MyInt新增一個SayHello的方法
func (m MyInt) SayHello() {
    fmt.Println("Hello, 我是一個int。")
}
func main() {
    var m1 MyInt
    m1.SayHello() //Hello, 我是一個int。
    m1 = 100
    fmt.Printf("%#v  %T\n", m1, m1) //100  main.MyInt
}

3.1 方法的宣告和呼叫

方法的宣告語法:

//宣告一個自定義型別struct
type A struct {
    Num int
}

//宣告A型別的方法
func (a A) test() {     //其中的(a A)表示test方法和A型別繫結
    fmt.Println(a.Num)
}

舉例說明:

type Person struct {
    Name string
}

func (p Person) test() {
    fmt.Println("test() name=", p.Name)
}

func main() {
    var p Person    //例項化
    p.Name = "tom"
    p.test()    //呼叫方法
}
  • test方法和Person型別繫結;

  • test方法只能通過Person型別的變數來呼叫,不能直接呼叫,也不能使用其它型別變數來呼叫;

  • func (p Person) test() {...},其中p表示哪個Person變數呼叫,這個p就是它的副本,代表接收者。這點和函式傳參非常相似,並且p可以有程式設計師任意指定;

3.2 方法的傳參機制

在3.1中 提到,方法要和指定自定義型別的變數繫結,那個這個繫結方法的變數被稱為接收者,而方法的傳參機制被這個接收者的型別不同分為值型別的接收者和指標型別的接收者,下面分別來看這這兩方式的傳參機制:

3.2.1 值型別的接收者

指標型別的接收者由一個結構體的指標組成,由於指標特性,呼叫方法時修改接收者的任意成員變數,在方法接收後,修改都是有效的。例如為Person結構體新增一個SetAge方法,來修改例項中的年齡:

//Person 結構體
type Person struct {
    name string
    age  int8
}

// SetAge 設定p的年齡
// 使用指標接收者
func (p *Person) SetAge(newAge int8) {
    p.age = newAge
}

func main() {
    p1 := NewPerson("小王子", 25)
    fmt.Println(p1.age) // 25
    p1.SetAge(30)
    fmt.Println(p1.age) // 30
}

3.2.2 指標型別的接收者

當方法和值型別接收者繫結是,Go語言會在程式碼執行時將接收者的值複製一份 。在值型別接收者的方法中可以獲取接收者的成員值,但修改操作只是針對副本,無法修改接收者變數本身。

// SetAge2 設定p的年齡
// 使用值接收者
func (p Person) SetAge2(newAge int8) {
    p.age = newAge
}

func main() {
    p1 := NewPerson("張三", 25)
    p1.Dream()
    fmt.Println(p1.age) // 25
    p1.SetAge2(30) // (*p1).SetAge2(30)
    fmt.Println(p1.age) // 25
}

3.2.3 使用指標型別傳參的時機

  • 需要修改接收者中的值;
  • 傳參時拷貝代表比較大的大例項;
  • 保證形參例項和實參例項的一致性,如果有某個方法使用了指標接收者,那麼其他的方法也應該使用指標接收者。

3.3 方法和函式區別

  • 宣告方式不一樣
    • 函式的宣告方式:func 函式名(形參列表) (返回值列表) {...}
    • 方法的宣告方式:func (變數 自定義型別) 函式名(形參列表) (返回值列表) {...}
  • 呼叫方式不一樣
    • 函式的呼叫方式:函式名(實參列表)
    • 方法的呼叫方式:變數.方法名(實參列表)
  • 對於普通函式,接收者為值型別時,不能講指標型別的資料直接傳遞,反之亦然;
type Person struct {
    Name string
}

func test01(p Person) {
    fmt.Println(p.Name)
}

func test02(p *Person) {
    fmt.Println(p.Name)
}

func main() {
    var p = Person{"tom"}
    test01(p)
    test02(&p)
}
  • 對於方法(如struct的方法),接收者為值型別時,可以直接用指標型別的變數呼叫方法,反之亦然;
func (p Person) test03() {
    p.Name = "jack"
    fmt.Println(p.Name)     //jack
}

func (p *Person) test03() {
    p.Name = "jerry"
    fmt.Println(p.Name)     //jerry
}

func main() {
    p := Person{"viktor"}
    p.test03()
    fmt.Println(p.Name)     // viktor
    
    (&p).test03()   //從形式上傳入地址,但是本質扔然是值拷貝
    fmt.Println(p.Name)     // viktor
    
    (&p).test04()
    fmt.Println(p.Name)     // jerry
    p.test04()  //等價於(&p).test04(),從形式上是傳入值型別,但是本事仍然是地址拷貝
}

對於方法來說,不管呼叫形式如何,真正決定是之拷貝還是地址拷貝,看這個方法是和哪種型別繫結,也就是接收者的型別是值型別還是指標型別。

4 工廠模式

Golang的結構體沒有建構函式,通常可以使用工廠模式來解決這個問題。

4.1 為何需要工廠模式

首選,在Golang語言中公有和私有變數這一說法。如果說一個包中的變數的首字母為小寫,在其他包如果引入這個包,就不能訪問這個變數;如果這個變數的變數名為大寫字母,那麼可以直接訪問。

同樣對於自定義的struct型別也是,而工廠模式,就是為了解決變數的首字母為小寫的結構體能夠被其它包引用的問題。

4.2 工廠模式的使用

//student.go屬於model包
package model

//定義一個結構體
type student struct {
    Name string
    Score float64
}

func NewStudent(n string, s float64) *stuent {
    return &student{
        Name : n,
        Score : s,
    }
}

//mian.go中宣告一個student例項,並初始化
func main() {
    var stu = model.NewStudent("viktor", 86.6)
    
    fmt.Println(*stu)
    fmt.Println("name=", stu.Name, "score=", stu.Score)
}

另外,如果結構體中的某個欄位的首字母也為小寫該如何訪問?

//student.go屬於model包
package model

//定義一個結構體
type student struct {
    Name string
    score float64
}

func NewStudent(n string, s float64) *stuent {
    return &student{
        Name : n,
        Score : s,
    }
}

func (s *student) GetScore() float64 {
    return s.score
}

//mian.go中宣告一個student例項,並初始化
func main() {
    var stu = model.NewStudent("viktor", 86.8)
    
    fmt.Println(*stu)
    fmt.Println("name=", stu.Name, "score=", stu.GetScore())
}