go語言的結構體
簡介
go語言中沒有類的概念,也不支持類的繼承等面向對象的概念, 但在結構體的內嵌配合接口比面向對象更具更高的擴展性和靈活性。
Go語言中的基礎數據類型可以表示一些事物的基本屬性,但是當我們想表達一個事物的全部或部分屬性時,這時候再用單一的基本數據類型明顯就無法滿足需求了,Go語言提供了一種自定義數據類型,可以封裝多個基本數據類型,這種數據類型叫結構體struct
,通過結構體來實現面向對象。
定義
使用type
和struct
關鍵字來定義結構體,格式如下:
type 類型名 struct {
字段名 字段類型
字段名 字段類型
…
}
其中:
- 類型名:表示自定義結構體的名稱,在同一個包內不能重復。
- 字段名:表示結構體字段名,結構體中的名字必須唯一。
- 字段類型:表示結構體字段的具體類型。
舉個例子,定義一個坐標結構體:
type Point struct {
X int
Y int
}
同種類型的字段也可以放一行,定義一個顏色的三原色結構體:
type Color struct {
R, G, B byte
}
結構體的實例化
結構體的定義只是一種內存布局的描述,此時並不會分配內存,只有實例化的時候才會真正分配內存。
在定義結構體並實例化才能使用結構體字段,有多重實例方式。
基本實例化
結構體本身是一種類型,可以像聲明變量一樣,以 var 的方式聲明結構體即可完成實例化。
var 結構體實例 結構體類型
舉個例子:
package main
import (
"fmt"
)
type Person struct {
name string
city string
age int
}
func main() {
var p1 Person
p1.name = "江子牙"
p1.city = "江西"
p1.age = 23
// {江子牙 江西 23}
fmt.Println(p1)
// 類型:main.Person 值:main.Person{name:"江子牙", city:"江西", age:23}
fmt.Printf("類型:%T\t值:%#v", p1, p1)
}
我們可以通過.
來對結構體的字段進行賦值和取值操作。
創建指針類型的結構體
我們還可以通過使用new
關鍵字對結構體進行實例化,得到的是結構體的地址。 格式如下:
package main
import (
"fmt"
)
type Person struct {
name string
city string
age int
}
func main() {
p1 := new(Person)
// &{ 0} 得到的是一個結構體指針,字段的值對應該字段類型的零值
fmt.Println(p1)
// 指針:0xc00006e2d0 類型:*main.Person 值:&main.Person{name:"", city:"", age:0}
fmt.Printf("指針:%p\t類型:%T\t值:%#v", p1, p1, p1)
}
盡管得到的是一個結構體指針,但是go語言中同樣可以通過.
來進行對字段進行操作:
package main
import (
"fmt"
)
type Person struct {
name string
city string
age int
}
func main() {
p1 := new(Person)
p1.name = "江子牙"
p1.city = "江西"
p1.age = 23
// &{江子牙 江西 23}
fmt.Println(p1)
// 指針:0xc00006e2d0 類型:*main.Person 值:&main.Person{name:"江子牙", city:"江西", age:23}
fmt.Printf("指針:%p\t類型:%T\t值:%#v", p1, p1, p1)
}
取結構體的地址實例化
使用&
對結構體進行取地址操作相當於對結構體進行了一次new
操作。
package main
import (
"fmt"
)
type Person struct {
name string
city string
age int
}
func main() {
p2 := &Person{}
p2.name = "江子牙"
p2.city = "江西"
p2.age = 23
// &{江子牙 江西 23}
fmt.Println(p2)
// 指針:0xc00006e2d0 類型:*main.Person 值:&main.Person{name:"江子牙", city:"江西", age:23}
fmt.Printf("指針:%p\t類型:%T\t值:%#v", p2, p2, p2)
}
結構體的初始化
鍵值對初始化
類似於Python的關鍵字傳參
package main
import (
"fmt"
)
type Person struct {
name string
age int
isLogin bool
}
func main() {
p1 := Person{
name:"江子牙",
age:23,
isLogin:true,
}
// {江子牙 23 true}
fmt.Println(p1)
}
值的列表初始化
類似於Python的位置參數
package main
import (
"fmt"
)
type Person struct {
name string
age int
isLogin bool
}
func main() {
p2 := Person{
"江子牙", 23, true,
}
// {江子牙 23 true}
fmt.Println(p2)
}
使用此方式初始化時,值得注意的是:
- 必須初始化結構體的所有字段
- 初始化順序必須和結構體聲明時保持一致
- 兩種方式的初始化不能混用
匿名結構體的初始化
顧名思義,匿名結構體就是沒有名字的結構體,無需用type
關鍵字聲明就可以直接使用。
package main
import (
"fmt"
)
func main() {
s1 := struct {
name string
age int
}{
"江子牙",
23,
}
// 江子牙
fmt.Println(s1.name)
// 23
fmt.Println(s1.age)
// {江子牙 23}
fmt.Println(s1)
}
構造函數
結構體沒有構造函數,我們可以自己實現。因為結構體是值類型,如果結構體較復雜的話,值拷貝的性能開銷會比較大,所以我們的構造函數返回結構體指針類型。
package main
import "fmt"
type Cat struct {
Name string
Color string
}
func NewCat(name, color string) *Cat {
return &Cat{
Name: name,
Color: color,
}
}
func main() {
// 調用構造函數實例化一個小豹子
cat := NewCat("小豹子", "豹紋")
// &{小豹子 豹紋}
fmt.Println(cat)
// 小豹子
fmt.Println(cat.Name)
// 豹紋
fmt.Println(cat.Color)
}
方法Method
go語言中的方法method是一種作用於特定類型變量的函數,叫做receiver
,可以理解為其他語言的this
和self
。
格式如下:
func (接收器變量 接收器類型) 方法名 (參數列表) (返回值列表) {
函數體
}
- 接收器變量:官方建議,該接收器變量最好和接收器類型的第一個字母保持一致。如 Cat c 、Bag b 、Dog d。
- 接收者類型:可以是指針類型和非指針類型。
- 方法名、參數列表、返回值:與函數定義相同。
package main
import "fmt"
// 定義一個包的結構體
type Bag struct {
items []interface{}
}
// 為結構體定義一個Insert方法
func (b *Bag) Insert(thing interface{}) {
b.items = append(b.items, thing)
}
func main() {
// 實例化一個包
bag := new(Bag)
// 放入一個東西
bag.Insert("口紅")
// [口紅]
fmt.Println(bag.items)
// &{[口紅]}
fmt.Println(bag)
// 放入一個手機
bag.Insert("手機")
// [口紅 手機]
fmt.Println(bag.items)
// &{[口紅 手機]}
fmt.Println(bag)
}
接收者
指針類型的接收者
指針類型的接收器是一個結構體的指針,更接近於面向對象的this or self。
由於指針的特性,調用方法時,修改接收器指針的任意成員變量,在方法調用結束后,已經修改過得都是有效的。
上個例子的接收器*Bag就是一個指針接收器,所以才能append之后保持修改。
值類型的接收者
當方法作用與非指針接收器時,go語言內部會在代碼運行的時候將接收器的數據復制一份,在非指針接收器的方法可以獲取,也可以修改,只是修改無效而已。
package main
import "fmt"
type Bag struct {
name string
}
func (b *Bag) SetName(name string) {
b.name = name
}
func (b Bag) SetName1(name string) {
fmt.Println(name, "修改不生效")
b.name = name
}
func main() {
// 實例化一個包
bag := new(Bag)
// 設置
bag.SetName("指針類型的接收者")
// 生效了:指針類型的接收者
fmt.Println(bag.name)
// 再設置
bag.SetName1("值類型的接收者")
// 不生效:指針類型的接收者
fmt.Println(bag.name)
}
總結
-
需要修改接收者中的值
-
接收者是拷貝代價比較大的大對象
-
保證一致性,如果有某個方法使用了指針接收者,那么其他的方法也應該使用指針接收者。
為任意類型添加方法
go語言可以對任何一種類型添加方法。給一種類型添加方法就像給結構體添加方法一樣。
package main
import "fmt"
// 定義一個MyInt類型
type MyInt int
// 為 MyInt 添加是否為0的方法
func (m MyInt) IsZero() bool {
return m == 0
}
// 為 MyInt 添加add()方法
func (m MyInt) Add(other int) int {
return int(m) + other
}
func main() {
var b MyInt
// 定義之后為零值
fmt.Println(b.IsZero())
b = 100
// 賦值之后不為0
fmt.Println(b.IsZero())
// 兩數相加:100+150
fmt.Println(b.Add(150))
}
結構體的匿名字段
結構體允許字段沒有字段名,只有類型。這種字段就叫做匿名字段。
package main
import "fmt"
// 定義一個匿名字段的結構體
type Person struct {
string
int
}
func main() {
p1 := &Person{
"江子牙",
23,
}
// &{江子牙 23}
fmt.Println(p1)
}
嵌套結構體
一個結構體中可以嵌套包含另一個結構體或結構體指針。
比如:
package main
import "fmt"
// 定義一個學生的結構體
type Student struct {
name string
age int
*Class // 嵌套一個匿名結構體的指針
School // 嵌套一個匿名結構體
}
// 定義一個班級結構體
type Class struct {
className string
}
// 定義一個學校結構體
type School struct {
schoolName string
}
func main() {
s1 := &Student{
"江子牙",
23,
&Class{
"高三一班",
},
School{"明珠中學"},
}
// &{江子牙 23 0xc0000421c0 {明珠中學}}
fmt.Println(s1)
// 高三一班
fmt.Println(s1.className)
// 明珠中學
fmt.Println(s1.schoolName)
}
學生結構體嵌套了班級和學校兩個結構體,獲取班級學校和班級名字的時候,會一直往下找。可以通過.
的方式。
結構體的繼承
go語言沒有面向對象,更沒有繼承。只是可以實現同樣的效果。
package main
import "fmt"
// 定義一個兒子的結構體
type Child struct {
name string
*Father // 通過匿名結構體實現繼承
}
// 定義一個父親結構體
type Father struct {
money int64
}
// 為父親添加一個生小孩的方法
func (f *Father) Make(name string) {
fmt.Printf("%s生小孩", name)
}
func main() {
ch := &Child{
name:"小明",
Father:&Father{50000},
}
// 小明繼承的財產為:50000
fmt.Printf("%s繼承的財產為:%d\n",ch.name, ch.money )
// 小明生小孩
ch.Make("小明")
}
小明結構體中並沒有money
字段和Make
方法。這兩個都是他父親結構體中才有的,這樣一來便實現了繼承屬性和方法。
結構體字段的可見性
結構體中字段大寫開頭表示可公開訪問,小寫表示私有(僅在定義當前結構體的包中可訪問)。
結構體json序列化
package main
import (
"encoding/json"
"fmt"
)
// 學生結構體
type Student struct {
ID int
Gender string
Name string
IsLogin bool
}
func main() {
var stu1 = Student{
ID: 1,
Gender: "男",
Name: "豪傑",
IsLogin: false,
}
// 序列化:把內存里的數據轉換為字符串,以便用於網絡傳輸和數據交換
if v, err := json.Marshal(stu1); err != nil {
fmt.Println("JSON格式化出錯")
fmt.Println(err)
} else {
// 字節類型的切片
fmt.Println(v)
// 轉為字符串
fmt.Println(string(v))
}
// 反序列化:把json字符串轉換為當前編程語言可用的對象
str := `{"ID":1,"Gender":"男","Name":"豪傑","IsLogin":false}`
stu2 := new(Student)
_ = json.Unmarshal([]byte(str), stu2)
fmt.Println(stu2)
fmt.Printf("stu2 類型:%T\tstu2值:%#v", stu2, stu2)
}
結構體的標簽
Tag
是結構體的元信息,可以在運行的時候通過反射的機制讀取出來。 Tag
在結構體字段的后方定義,由一對反引號包裹起來,具體的格式如下:
`key1:"value1" key2:"value2"`
結構體標簽由一個或多個鍵值對組成。鍵與值使用冒號分隔,值用雙引號括起來。鍵值對之間使用一個空格分隔。
注意事項: 為結構體編寫Tag
時,必須嚴格遵守鍵值對的規則。結構體標簽的解析代碼的容錯能力很差,一旦格式寫錯,編譯和運行時都不會提示任何錯誤,通過反射也無法正確取值。例如不要在key和value之間添加空格。
因為序列化和反序列化大多是與前端進行數據交互。不能給他返回一些大寫的鍵。所以可以通過tag
來解決。
package main
import (
"encoding/json"
"fmt"
)
//Student 學生
type Student struct {
ID int `json:"id"` //通過指定tag實現json序列化該字段時的key
Gender string //json序列化是默認使用字段名作為key
name string //私有不能被json包訪問
}
func main() {
s1 := Student{
ID: 1,
Gender: "男",
name: "沙河娜扎",
}
data, err := json.Marshal(s1)
if err != nil {
fmt.Println("json marshal failed!")
}
//json str:{"id":1,"Gender":"男"}
fmt.Printf("json str:%s\n", data)
}