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}
}
繼承帶來的便利
代碼的復用性提高了
代碼的擴展性和維護性提高了
結構體使用注意事項
-
結構體是用戶單獨定義的類型,和其它類型進行轉換時,需要有完全相同的字段(名字、個數和類型),示例如下:
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) }
-
結構體是值類型,在方法調用中遵守值類型的傳遞機制,是值拷貝傳遞方式
-
如程序員希望在方法中,修改結構體變量的值,可以通過結構體指針的方式來處理
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) //輸出小喬 }
-
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() }
-
方法的訪問范圍控制的規則,和函數一樣,方法名首字母小寫,只能在本包訪問,方法首字母大寫,可以在本包和其它包訪問。
-
如果一個類型實現了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) }