Go语言中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了,Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名称struct
。 也就是我们可以通过struct
来定义自己的类型了。
Go语言中通过struct
来实现面向对象。
结构体的定义
基本结构体
使用type
和struct
来定义结构体,基本语法如下:
type 类型名 struct{
字段名 字段类型
字段名 字段类型
}
其中:
- 类型名:标识自定义结构体的名称,在同一个包内不能重复。
- 字段名:表示结构体字段名。结构体中的字段名必须唯一。
- 字段类型:表示结构体字段的具体类型。
比如,定义一个Person
的结构体:
type Person struct{
name string
age int
}
匿名结构体
在定义一些临时数据结构的时候可以用匿名结构体,基本语法如下:
var 类型名 struct{
字段名 字段类型
字段名 字段类型
}
和基本结构体相比,匿名结构体是用var
来声明匿名结构体。
比如定义下面匿名结构体:
var user struct{
name string
age int
}
结构体实例化
只有当结构体实例化时,才会真正地分配内存。也就是必须实例化后才能使用结构体的字段。
结构体本身也是一种类型,我们可以像声明内置类型一样使用var
关键字声明结构体类型。
var 结构体实例 结构体类型
基本实例化
示例如下:
package main
import "fmt"
type person struct {
name string
age int
}
func main() {
// 结构体实例化
var joker person
joker.name = "Joker"
joker.age = 18
fmt.Println(joker)
}
我们通过.
来访问结构体的字段(成员变量),例如joker.name
和joker.age
等。
实例化为指针类型结构体
我们可以通过new
关键字对结构体进行实例化,得到的是结构体的地址。如下:
package main
import "fmt"
type person struct {
name string
age int
}
func main() {
// 结构体实例化
var p2 = new(person)
fmt.Printf("%T\n", p2) // *main.Person
}
从输出结果来看p2
是一个指针类型。
指针类型的结构体依然是用.
来访问结构体的成员,如下:
package main
import "fmt"
type person struct {
name string
age int
}
func main() {
// 结构体实例化
var p2 = new(person)
fmt.Printf("%T\n", p2) // *main.Person
p2.name = "乔克"
p2.age = 20
fmt.Printf("%v\n", p2) // &{乔克 20}
fmt.Println(*p2) // {乔克 20}
}
取结构体的地址除了使用new
方法外还可以使用&
。如下:
package main
import "fmt"
type person struct {
name string
age int
}
func main() {
// 结构体实例化
p3 := &person{}
fmt.Printf("%T\n", p3)
p3.name = "Jim"
p3.age = 45
fmt.Printf("%v\n", p3) // &{Jim 45}
fmt.Println(*p3) // {Jim 45}
}
结构体初始化
没有初始化的结构体,其成员变量都是零值,如下:
package main
import "fmt"
type person struct {
name string
age int
}
func main() {
// 结构体实例化
var p4 person
fmt.Printf("p4:%##v\n", p4) //p4:main.person{name:"", age:0}
}
如果要进行初始化,有以下几种。
使用键值对初始化
使用键值对对结构体进行初始化时,键对应结构体的字段,值对应该字段的初始值。如下:
package main
import "fmt"
type person struct {
name string
age int
}
func main() {
// 结构体实例化
p5 := person{
name: "小白",
age: 18,
}
fmt.Printf("p5:%#v\n", p5) //p5:main.person{name:"小白", age:18}
}
也可以对结构体指针进行键值对初始化,如下:
package main
import "fmt"
type person struct {
name string
age int
}
func main() {
// 结构体实例化
p6 := &person{
name: "小青",
age: 100,
}
fmt.Printf("p6:%#v\n", p6) //p6:&main.person{name:"小青", age:100}
}
如果某些键值对不需要,我们可以省略,该键值对就会是默认零值,如下:
package main
import "fmt"
type person struct {
name string
age int
}
func main() {
// 结构体实例化
p7 := person{
name: "小灰",
}
fmt.Printf("p7:%#v\n", p7) //p7:main.person{name:"小灰", age:0}
}
使用值的列表初始化
初始化结构体的时候可以简写,也就是初始化的时候不写键,直接写值。如下:
package main
import "fmt"
type person struct {
name string
age int
}
func main() {
// 结构体实例化
p8 := person{
"小红",
23,
}
fmt.Printf("p8:%#v\n", p8) //p8:main.person{name:"小红", age:23}
}
使用这种格式初始化时,需要注意:
- 必须初始化结构体的所有字段。
- 初始值的填充顺序必须与字段在结构体中的声明顺序一致。
- 该方式不能和键值初始化方式混用。
结构体内存布局
结构体在内存中是占用一块连续得内存,我们这里以int8
类型为例,因为int8
类型在内存中刚好占一个字节。
如下定义一个结构体,然后打印其内存指针。
package main
import "fmt"
// 定义一个全是int8类型得结构体
type memTest struct {
a int8
b int8
c int8
d int8
}
func main() {
// 初始化结构体
s := memTest{1, 2, 3, 4}
fmt.Printf("s.a:%p\n", &s.a) //s.a:0xc000064068
fmt.Printf("s.b:%p\n", &s.b) //s.b:0xc000064069
fmt.Printf("s.c:%p\n", &s.c) //s.c:0xc00006406a
fmt.Printf("s.d:%p\n", &s.d) //s.d:0xc00006406b
}
构造函数
Go语言的结构体没有构造函数,我们可以自己实现。 例如,下方的代码就实现了一个person
的构造函数。 因为struct
是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型。
package main
import "fmt"
type person struct {
name string
age int
}
func newPersion(name string, age int) *person {
return &person{
name: name,
age: age,
}
}
func main() {
// 调用构造函数
p9 := newPersion("小乖乖", 20)
fmt.Printf("p9:%#v\n", p9)
}
构造函数的命名规则:约定成俗是new
+结构体名
,如上newPerson
。
方法和接收者
Go语言中的方法(Method)
是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)
。接收者的概念就类似于其他语言中的this
或者 self
。
方法的定义格式如下:
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
函数体
}
其中,
- 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是
self
、this
之类的命名。例如,Person
类型的接收者变量应该命名为p
,Connector
类型的接收者变量应该命名为c
等。 - 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
- 方法名、参数列表、返回参数:具体格式与函数定义相同。
例如:
package main
import "fmt"
type person struct {
name string
age int
}
// 构造函数
func newPersion(name string, age int) *person {
return &person{
name: name,
age: age,
}
}
// 构造方法
func (p person) buyHouse(name string) {
fmt.Printf("%s买房子了", name)
}
func main() {
// 结构体实例化
p9 := newPersion("小乖乖", 20)
fmt.Printf("p9:%#v\n", p9) //p9:&main.person{name:"小乖乖", age:20}
p9.buyHouse(p9.name) //小乖乖买房子了
}
方法与函数的区别是,函数不属于任何类型,方法属于特定的类型。
指针类型的接收者
由于结构体是值类型
,在给函数传参的时候是值传递,相当于ctrl+c
和ctrl+v
,其修改只在当前方法中有效。如果我们要使其在方法结束后依然有效,就需要使用到指针类型,因为其传递的是内存指针。
如下:
package main
import "fmt"
type person struct {
name string
age int
}
// 构造函数
func newPersion(name string, age int) *person {
return &person{
name: name,
age: age,
}
}
// 构造方法
func (p person) buyHouse() {
fmt.Printf("%s买房子\n", p.name)
}
// 指针类型的 接收者
func (p *person) changeAge(age int) {
p.age = age
}
func main() {
// 结构体实例化
p9 := newPersion("小乖乖", 20)
fmt.Printf("p9:%#v\n", p9) //p9:&main.person{name:"小乖乖", age:20}
p9.buyHouse() //小乖乖买房子了
p9.changeAge(21)
fmt.Printf("p9:%#v\n", p9) //p9:&main.person{name:"小乖乖", age:21}
}
通过changeAge
修改值对其本身p9
也生效了。
值类型的接收者
我们现在定义一个值类型的接收者和上面的指针类型接收者做个对比。如下:
package main
import "fmt"
type person struct {
name string
age int
}
// 构造函数
func newPersion(name string, age int) *person {
return &person{
name: name,
age: age,
}
}
// 构造方法
func (p person) buyHouse() {
fmt.Printf("%s买房子\n", p.name)
}
// 指针类型的 接收者
func (p *person) changeAge(age int) {
p.age = age
}
// 值类型的接收者
func (p person) changeAge2(age int) {
p.age = age
}
func main() {
// 结构体实例化
p9 := newPersion("小乖乖", 20)
fmt.Printf("p9:%#v\n", p9) //p9:&main.person{name:"小乖乖", age:20}
p9.buyHouse() //小乖乖买房子了
p9.changeAge(21)
fmt.Printf("p9:%#v\n", p9) //p9:&main.person{name:"小乖乖", age:21}
p9.changeAge2(100)
fmt.Printf("p9:%#v\n", p9) //p9:&main.person{name:"小乖乖", age:21}
}
定义值类型的接收者的方法changeAge2
,我们在调用后,输出p9
,发现其实并没有改变。从上面就可以看出值类型和指针类型接收者的区别。
如何选择
- 需要修改接收者中的值
- 接收者是拷贝代价比较大的大对象
- 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。
任意类型添加方法
在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。 举个例子,我们基于内置的int
类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。
//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
}
注意事项:
- 非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法。
- 我们不能给内置的类型添加方法。
结构体的匿名字段
前面说过匿名就是没写名字,那么结构体的匿名字段就是在结构体中没写名字的字段。如下:
// 定义匿名字段的结构体
type test struct{
string
int
}
匿名字段的结构体该如何使用呢?如下:
package main
import "fmt"
// 定义匿名字段的结构体
type test struct {
string
int
}
func main() {
// 声明结构体变量,并实例化
s1 := test{
"小黑",
500,
}
// 取变量中的值
fmt.Printf("%s - %d\n", s1.string, s1.int) //小黑 - 500
}
声明匿名字段的结构体和通过值得列表得方式声明普通结构体类似,字段从上到下是什么类型就写什么类型。
在取变量中得值得时候直接使用类型名。可以理解为匿名字段默认就以类型名作为字段名。
既然匿名字段得结构体是直接用得类型,那么就可以想到在一个匿名字段结构体中的字段必须唯一,所以在一个结构体中同种类型的匿名字段只能有一个。
嵌套结构体
顾名思义,嵌套结构体就是在结构体中嵌套另一个结构体。如下:
package main
import "fmt"
type address struct {
city string
street string
}
type company struct {
city string
street string
}
type person struct {
name string
age int
addr address
comp company
}
func main() {
// 声明
p := person{
name: "小白",
age: 20,
addr: address{
city: "重庆",
street: "观音桥",
},
comp: company{
city: "重庆",
street: "大坪",
},
}
fmt.Printf("p:%#v\n", p) // p:main.person{name:"小白", age:20, addr:main.address{city:"重庆", street:"观音桥"}, comp:main.company{city:"重庆", street:"大坪"}}
}
我们在person的结构体中嵌套了address和company结构体,这就是结构体的嵌套。
如果我们要取结构体中结构体的值,可以用如下方式:
fmt.Printf("%s\n", p.addr.city)
嵌套匿名字段的结构体
结构体还可以嵌套匿名字段的结构体,如下:
package main
import "fmt"
type address struct {
city string
area string
street string
}
type company struct {
city string
street string
}
type person struct {
name string
age int
address
company
}
func main() {
// 声明
p := person{
name: "小白",
age: 20,
address: address{
city: "重庆",
area: "江北区",
street: "观音桥",
},
company: company{
city: "重庆",
street: "大坪",
},
}
fmt.Printf("p:%#v\n", p)
fmt.Printf("%s\n", p.area)
}
结构体嵌套匿名字段结构体,如果没有字段冲突可以直接用语法糖
的形式访问,如下:
fmt.Printf("%s\n", p.area)
如果有字段冲突,就要用如下方式:
fmt.Printf("%s\n",p.address.city)
fmt.Printf("%s\n",p.company.city)
结构体的”继承”
在Go中没有继承这一说法,但是可以实现其他编程语言中面向对象中的继承。如下:
package main
import "fmt"
// 定义一个animal结构体
type animal struct {
name string
}
// 定义一个animal结构体的方法
func (a *animal) run() {
fmt.Printf("%s是可以跑的\n", a.name)
}
// 定义一个dog结构体
type dog struct {
*animal // 通过嵌套匿名结构体实现集成
age int
}
// 定义一个dog结构体的方法
func (d *dog) eat() {
fmt.Printf("%s可以吃东西\n", d.name)
}
func main() {
// 定义一个指针类型的dog
d := &dog{
animal: &animal{
name: "小黑",
},
age: 2,
}
d.run() // 可以调用animal的方法
d.eat() // 可以调用自己的方法
/*
输出:
小黑是可以跑的
小黑可以吃东西
*/
}
结构体字段的可见性
结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。
如果结构体的的名字是大写字母开头,则需要在上面注释才不会出现警告信息,格式如下:
// Dog 定义一个Dog的结构体
type Dog struct{
name string
}
结构体与JSON
其他语言都有JSON序列化和反序列化,比如python的json库。在Go语言也t有类似库,它是encoding/json
。详情可以查看:[https://studygolang.com/pkgdoc]。
例子:
package main
import (
"encoding/json"
"fmt"
)
// 定义一个animal结构体
type animal struct {
name string
}
// 定义一个dog结构体
type dog struct {
*animal // 通过嵌套匿名结构体实现集成
age int
}
func main() {
// 声明结构体变量
d := &dog{
animal: &animal{
name: "小七",
},
age: 1,
}
// 进行JSON序列化:结构体 -> json
data, err := json.Marshal(d)
if err != nil {
fmt.Printf("结构体转换为JSON失败:%#v\n", err)
} else {
fmt.Printf("结构体 -> json:%#v\n", string(data)) //结构体 -> json:"{}"
}
}
上面的输出结果并不是我们想要的,这是为什么呢?
我们前面介绍过函数之间或者包之间的变量传递是值传递,我们是在json
这个包里做的序列化,然后在我们main
包进行使用,是拿不到json
中的数据的,如果要拿到,则结构体中的字段名用大写,如下:
package main
import (
"encoding/json"
"fmt"
)
// 定义一个animal结构体
type animal struct {
Name string
}
// 定义一个dog结构体
type dog struct {
*animal // 通过嵌套匿名结构体实现集成
Age int
}
func main() {
// 声明结构体变量
d := &dog{
animal: &animal{
Name: "小七",
},
Age: 1,
}
// 进行JSON序列化:结构体 -> json
data, err := json.Marshal(d)
if err != nil {
fmt.Printf("结构体转换为JSON失败:%#v\n", err)
} else {
fmt.Printf("结构体 -> json:%#v\n", string(data)) //结构体 -> json:"{\"Name\":\"小七\",\"Age\":1}"
}
}
上面说的是序列化,那么反序列化就是从json
文件转化为结构体,如下:
// 自定义一个json数据
str1 := `{"animal":{"name":"小白"},"age":100}`
// 定义一个结构体接收数据
d1 := dog{}
// 将其转化为结构体
err = json.Unmarshal([]byte(str1), &d1)
if err != nil {
fmt.Printf("%#v\n", err)
}
fmt.Printf("%#v\n", d1)
注意:
- 函数传参是值传递,所以给Unmarshal的接收变量得是指针变量
结构体标签
Tag
是结构体的元信息,可以在运行的时候通过反射的机制读取出来。 Tag
在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下:
`key1:"value1" key2:"value2"`
结构体标签由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。键值对之间使用一个空格分隔。 注意事项: 为结构体编写Tag
时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在key和value之间添加空格。
如下:
package main
import (
"encoding/json"
"fmt"
)
// 定义一个animal结构体
type animal struct {
Name string `json:"name"`
}
// 定义一个dog结构体
type dog struct {
*animal // 通过嵌套匿名结构体实现集成
Age int `json:"age"`
}
func main() {
// 声明结构体变量
d := &dog{
animal: &animal{
Name: "小七",
},
Age: 1,
}
// 进行JSON序列化:结构体 -> json
data, err := json.Marshal(d)
if err != nil {
fmt.Printf("结构体转换为JSON失败:%#v\n", err)
} else {
fmt.Printf("结构体 -> json:%#v\n", string(data)) //结构体 -> json:"{\"name\":\"小七\",\"age\":1}"
}
}