Go語言結構


Go通過類型別名(alias types)和結構體的形式支持用戶自定義類型,或者叫定制類型。一個帶屬性的結構體試圖表示一個現實世界中的實體。結構體是復合類型(composite types),當需要定義一個類型,它由一系列屬性組成,每個屬性都有自己的類型和值的時候,就應該使用結構體,它把數據聚集在一起。然后可以訪問這些數據,就好像它是一個獨立實體的一部分。結構體也是值類型,因此可以通過new函數來創建

組成結構體類型的那些數據稱為字段(fields)。每個字段都有一個類型和一個名字;在一個結構體中,字段名字必須是唯一的。

我們為什么需要結構體?

編程語言,終究是為了解決我們現實生活中的問題,生活中的事物(如人)都有屬性(嘴、胳膊)和方法(行走、跳躍)。使用之前的普通類型和數組來表示這些是不方便的,結構體就像其它編程語言中的類(class),包含了一系列的屬性和方法,結構體能夠更好的描述事物,更好的解決問題。

tips:屬性即字段

結構體定義

結構體定義的一般方式如下:

type identifier struct {
    field1 type1
    field2 type2
    ...
}

示例:

type Student struct {
    Name  string
    Age   int
    Score float64
}

type T struct{a,b int}也是合法的語法,它更適用於簡單的結構體。

結構體的字段可以是任何類型,甚至是結構體本身,也可以是函數或者接口。可以聲明結構體類型的一個變量,然后像下面這樣給它的字段賦值:

var s T
s.a = 6
s.b = 8

數組可以看作是一種結構體類型,不過它使用下標而不是具名的字段。

創建結構體實例

創建結構體實例有兩種方式,一種是普通方式(var t T)創建,另一種是使用new()方法來創建對應結構體的實例。

普通方式創建結構體實例

聲明var t T會給t分配內存,並零值化內存,這個時候t的類型是T。語法如下

var t T

示例:

package main

import "fmt"

type Person struct {
	Name string
	age  int
}

func main() {

	var p Person

	p = Person{Name:"黃忠", age:35}

	fmt.Println(p)
}

new()創建結構體實例

使用 new 函數給一個新的結構體變量分配內存,它返回指向已分配內存的指針: var t *T = new(T) ,如果需要可以把這條語句放在不同的行(比如定義是包范圍的,但是分配卻沒有必要在開始就做)。

var t *T = new(T)

寫這條語句的慣用方法是:t := new(T),變量 t 是一個指向 T 的指針(t的類型是*T),此時結構體字段的值是它們所屬類型的零值。

示例:

package main

import "fmt"

type Person struct {
	Name string
	age  int
}

func main() {

	var p *Person = new(Person)

	(*p).Name = "黃忠" //也可以簡寫成 p.Name = "黃忠",編譯器在編譯時會優化自動幫我們加上
	(*p).age  = 34    //也可以簡寫成 p.age = 34

	fmt.Println(*p)
}

在創建一個結構體變量后,如果沒有給字段賦值,都對應一個零值(默認值)。在首次使用結構體里的引用字段的時候一定要先make()或者使用字面量的方式初始化才能再使用。

例如:

package main

import "fmt"

type Person struct {
	Name string
	Age  int
	Scores [5]float64
	ptr *int    //指針
	slice []int //切片
	mp map[string]string  //map
}

func main() {

	var p Person

	p.Name = "黃忠"
	p.Age  = 34
	p.Scores[0] = 12

	//使用引用類型字段,要先初始化內存,才能使用,或者使用字面量方式初始化
	p.ptr = new(int)
	*p.ptr = 4

	p.slice = []int{1, 2, 3, 4}

	p.mp = make(map[string]string)
	p.mp["test"] = "hello"

	fmt.Println(p)

}

結構體實例初始化

初始化實例的常規方式如下:

package main

import "fmt"

type Person struct {
	Name string
	age  int
}

func main() {

	var p3 Person
	p3.Name = "狄仁傑"
	p3.age  = 34
	fmt.Println(p3)
}

上面的代碼中使用了(.語法)來給屬性/字段賦值,在Go語言中這叫選擇器(selector),無論變量是一個結構體類型還是一個結構體類型指針,都使用同樣的 選擇器符(selector-notation) 來引用結構體的字段/屬性。(注:字段即屬性)

還有更簡單的方式就是使用混合字面量語法(composite literal syntax)

時間間隔(開始和結束時間以秒為單位)是使用結構體的一個典型例子:

type Interval struct {
    start int
    end   int
}
intr := Interval{0, 3}   //(A)
intr := Interval{end:5, start:1} //(B)
intr := Interval{end:5} //(C)

在(A)中,值必須以字段在結構體定義時的順序給出,& 不是必須的。(B)顯示了另一種方式,字段名加一個冒號放在值的前面,這種情況下值的順序不必一致,並且某些字段還可以被忽略掉,就像(C)中那樣。

示例:

package main

import "fmt"

type Person struct {
	Name string
	age  int
}

func main() {

	//方法一:混合字面量方法,
	var p Person
	p = Person{Name:"黃忠", age:35}

	//方法2:混合字面量方法(和上面的不同點是沒有指定屬性名,這里值的順序必須按照屬性順序來寫)
	var p2 Person
	p2 = Person{"小喬", 16}

	fmt.Println(p)
	fmt.Println(p2)

}

聊聊給結構體字段賦值

來看看下面的代碼:

package main

import "fmt"

type Person struct {
	Name string
	Age  int
}

func main() {

	var p *Person = new(Person)

	//對指針有了解的人都知道這里本來應該寫成(*p).Name = "黃忠" (*p).Age = 34
	//但是為什么寫成p.Name和p.Age也可以呢?
	//這是因為編譯器在內部幫我們做了優化,在編譯時會自動幫我們加上,讓我們在書寫代碼的時候更加的方便快捷
	//但是我們應當知道,這里本應該寫成(*p).Name = "黃忠" (*p).Age = 34
    //因為.運算符比*運算符優先級高,所以把*p用括號括起來
	p.Name = "黃忠"
	p.Age  = 34

	fmt.Println(*p)

}

結構體類型實例和指向它的指針內存布局

說明了結構體類型實例和一個指向它的指針的內存布局:

type Point struct {
    x int
    y int
}

使用new初始化:

作為結構體字面量初始化:

Go 語言中,結構體和它所包含的數據在內存中是以連續塊的形式存在的,即使結構體中嵌套有其他的結構體,這在性能上帶來了很大的優勢。不像 Java 中的引用類型,一個對象和它里面包含的對象可能會在不同的內存空間中,這點和 Go 語言中的指針很像。下面的例子清晰地說明了這些情況:

type Rect1 struct{Min, Max Point}
type Rect2 struct{Min, Max *Point}

結構體的方法

方法與函數及其類似,本質上是一樣的,只不過方法比函數多了接收者。也就是下圖中的(1)

示例:

定義了一個Person結構體,並且Person結構體定義了一個OutputName的方法,該方法屬於Person結構體,該方法只能通過Person結構體的實例來調用,

package main

import "fmt"

type Person struct {
	Name string
	Age  int
}

func (p Person) OutputName()  {
	fmt.Println(p.Name)
}
func main() {

	var test Person
	test.Name = "黃忠"
	test.OutputName()
}

面向對象

組合(繼承)

Go語言實現繼承的方式和其它大多數編程語言不太一樣,Go原因是通過組合來實現的。比如一個人具有姓名、年齡等屬性,而學生不僅具有該人類的各項屬性,而且還有分數,年級等額外屬性,此時就可以在學生結構體中組合人結構體來簡化代碼,代碼如下:

package main

import "fmt"

type Person struct {
	Name string //姓名
	Age  int    //年齡
}

type Student struct {
	Person
	score float64  //分數
	grade int  //年級
}

func main() {

	var stu Student

	stu.Name = "黃忠"  //stu.Person.Name = "黃忠"
	stu.Age  = 34     //stu.Person.Age  = 34
	stu.score = 99
	stu.grade = 11

	fmt.Println(stu) //輸出: {{黃忠 34} 99 11}
}

對上面代碼的小結:

(1) 當我們直接通過stu訪問字段或方式時,執行流程如下,比如stu.Name

(2) 編譯器會先看stu對應的類型有沒有Name,如果有,則直接調用Student類型的Name字段

(3) 如果沒有就去Student中嵌入的匿名結構體Person中有沒有聲明Name字段,如果有就調用,沒有沒有就繼續查找…如果找不到就報錯

(4) 當結構體匿名結構體有相同的字段或者方法時,編譯器采用就近訪問原則,如果希望訪問匿名結構體的字段和方法,可以通過匿名結構體名來區分,如stu.Person.Name

(5) 結構體嵌入兩個(或多個)匿名結構體,如兩個匿名結構體有相同的字段和方法(同時結構體本身沒有同名的字段和方法),在訪問時,就必須明確指定匿名結構體名字,否則編譯報錯。

(6) 如果一個 struct 嵌套了一個有名結構體,這種模式就是組合,如果是組合關系,那么在訪問組合的結構體的字段或方法時,必須帶上結構體的名字

import "fmt"

type Person struct {
	Name string //姓名
	Age  int    //年齡
}

type Student struct {
	person Person  //有名結構體
	score float64  //分數
	grade int  //年級
}

func main() {

	var stu Student

	stu.person.Name = "黃忠"
	stu.person.Age = 34
	stu.score = 99
	stu.grade = 11

	fmt.Println(stu) //輸出: {{黃忠 34} 99 11}
}

繼承帶來的便利

代碼的復用性提高了

代碼的擴展性和維護性提高了

結構體使用注意事項

  1. 結構體是用戶單獨定義的類型,和其它類型進行轉換時,需要有完全相同的字段(名字、個數和類型),示例如下:

    package main
    
    import "fmt"
    
    type A struct {
    	Num int
    }
    
    type B struct {
    	Num int
    }
    
    func main() {
    
    	var a A
    	var b B
    
    	b.Num = 3
    	a = A(b)
    
    	fmt.Println(a, b)
    }
    
  2. 結構體是值類型,在方法調用中遵守值類型的傳遞機制,是值拷貝傳遞方式

  3. 如程序員希望在方法中,修改結構體變量的值,可以通過結構體指針的方式來處理

    package main
    
    import "fmt"
    
    type Person struct {
    	Name string
    	Age  int
    }
    
    func (p *Person) modifyName()  {
    	p.Name = "小喬"
    }
    func main() {
    
    	var test Person
    	test.Name = "黃忠"
    	test.modifyName()
    
    	fmt.Println(test.Name) //輸出小喬
    }
    
  4. Go中的方法作用在指定的數據類型上的(即:和指定的數據類型綁定),因此自定義類型都可以有方法, 而不僅僅是struct,比如int, float64等都可以有方法

    package main
    
    import "fmt"
    
    type Integer int
    
    func (i Integer) print() {
    	fmt.Println("i = ", i)
    }
    
    func main() {
    
    	var i Integer = 4
    	i.print()
    }
    
  5. 方法的訪問范圍控制的規則,和函數一樣,方法名首字母小寫,只能在本包訪問,方法首字母大寫,可以在本包和其它包訪問。

  6. 如果一個類型實現了String()這個方法,那么fmt.Println默認會調用這個變量的String()進行輸出

    package main
    
    import "fmt"
    
    type Student struct {
    	Name string
    	Age  int
    }
    //給*Student實現方法String()
    func (stu *Student) String() string {
    	str := fmt.Sprintf("Name = [%v], Age = [%v]", stu.Name, stu.Age)
    	return str
    }
    
    func main() {
    
    	stu := Student{
    		Name: "Jane",
    		Age : 30,
    	}
    
    	//如果實現了*Student類型的String方法,就會自動調用
    	fmt.Println(&stu)
    }
    


免責聲明!

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



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