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())
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM