结构体
自定义类型
- 在Go语言中有一些基本的数据类型,如string、整型、浮点型、布尔等数据类型, Go语言中可以使用type关键字来定义自定义类型。
- 自定义类型是定义了一个全新的类型。我们可以基于内置的基本类型定义,也可以通过struct定义。例如:
//将MyInt定义为int类型
//通过type关键字的定义,MyInt就是一种新的类型,它具有int的特性。
type MyInt int
// 自定义一个person类型
type person struct {
name string
age int
}
类型别名
- 类型别名规定:TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型。就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人。
type TypeAlias = Type
- 我们之前见过的rune和byte就是类型别名,他们的定义如下:
type byte = uint8
type rune = int32
- 类型别名只在代码编写 过程中有效,编译完之后就不存在
- 自定义类型和类型别名 就差一个等号 可以理解位赋值
- 以type 开头的都是一个新的类型
- 自定义类型可以增加对应的类型方法。
- golang里面的方法是针对类型的函数,类似与 类里面的函数。只不过通过接收者定义。
结构体
Go语言中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了,Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名称struct。 也就是我们可以通过struct来定义自己的类型了。
- 结构体 struct是值类型
- Go语言中通过struct来实现面向对象。
- 结构体是一种数据类型,一种我们自己造的可以保存多个维度数据的类型.类似于其他语言的类,
- 结构体字段名首字母尽量用大写
type 类型名 struct {
字段名 字段类型
字段名 字段类型
…
}
type person struct {
name string
age int
addr string
}
- 类型名:标识自定义结构体的名称,在同一个包内不能重复。
- 字段名:表示结构体字段名。结构体中的字段名必须唯一。
- 字段类型:表示结构体字段的具体类型。
- 同样类型的字段也可以写在一行,如
name,addr string
这样 - 上面person方便保存一个人的信息了
- 语言内置的基础数据类型是用来描述一个值的,而结构体是用来描述一组值的
匿名结构体
多用于临时场景.
var a = struct{
x int
y int
}
结构体实例化与初始化
-
只有当结构体实例化时,才会真正地分配内存。也就是必须实例化后才能使用结构体的字段。
-
结构体本身也是一种类型,我们可以像声明内置类型一样使用var关键字声明结构体类型。
-
没有初始化的结构体,其成员变量都是对应其类型的零值。
-
当某些字段没有初始值的时候,该字段可以不写。此时,没有指定初始值的字段的值就是该字段类型的零值
-
也可以对结构体指针进行键值对初始化
type person struct {
name string
age int8
}
// 构造(结构体变量的)函数,返回值是对应的结构体类型
//通过一个函数 返回一个结构体 可能学习不到位 想不到有什么使用场景。
func newPerson(n string, a int) (p person) {
p = person{
name: n,
age: a,
}
return p
}
func main() {
var p1 person // 结构体实例化
p1.name = "张三"
p1.age = 30
p2 := person{"李四", 18} // 结构体实例化
p2 := person{name:"思理理", age:18} // 结构体实例化
// 调用构造函数生成person类型变量
p4 := newPerson("王五", 18)
p7 := &person{
city: "北京",
}
fmt.Printf("p7=%#v\n", p7) //p7=&main.person{name:"", city:"北京", age:0}
}
- 使用值的列表初始化
- 必须初始化结构体的所有字段。
- 初始值的填充顺序必须与字段在结构体中的声明顺序一致。
- 该方式不能和键值初始化方式混用。
p2 := person{"李四", 18} // 结构体实例化
- 结构体占用一块连续的内存。
创建指针型结构体
- 我们还可以通过使用new关键字对结构体进行实例化,得到的是结构体的地址
- 从打印的结果中我们可以看出p2是一个结构体指针。
语法糖
需要注意的是在Go语言中支持对结构体指针直接使用.
来访问结构体的成员。- 使用&对结构体进行取地址操作相当于对该结构体类型进行了一次new实例化操作
var p2 = new(person)
p2.name = "小王子"
p2.age = 28
fmt.Printf("%T\n", p2) //*main.person
fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"小王子", age:28}
p3 := &person{}
fmt.Printf("%T\n", p3) //*main.person
构造函数
Go语言的结构体没有构造函数,我们可以自己实现。 例如,下方的代码就实现了一个person的构造函数。 因为struct是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型。
// 构造(结构体变量的)函数,返回值是对应的结构体类型
//通过一个函数 返回一个结构体 可能学习不到位 想不到有什么使用场景。
func newPerson(n string, a int) (p *person) {
p = &person{
name: n,
age: a,
}
return p
}
func main() {
// 调用构造函数生成person类型变量
p4 := newPerson("王五", 18)
fmt.Printf("%#v\n", p4) //&main.person{name:"王五",age:18}
}
方法和接收者
- Go语言中的
方法(Method)
是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)
。接收者的概念就类似于其他语言中的this
或者self
- 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,
而不是self、this之类的命名
。例如,Person类型的接收者变量应该命名为 p,Connector类型的接收者变量应该命名为c等。 - 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
- 方法名、参数列表、返回参数:具体格式与函数定义相同。
- 其实就是在一般函数定义的时候 在方法名前面 添加一个小括号 里面写上
变量和变量类型
- 方法与函数的区别是:函数不属于任何类型,方法属于特定的类型。
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
函数体
}
- 举例 下面dream 只有person 类型可以使用。
//Person 结构体
type Person struct {
name string
age int8
}
//Dream Person做梦的方法
func (p Person) Dream() {
fmt.Printf("%s的梦想是学好Go语言!\n", p.name)
}
func main() {
p1 := Person("小王", 25)
p1.Dream() //小王的梦想是学好Go语言!
}
指针类型接收者 和值类型接收者
-
值类型接收者
:当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身。 -
指针类型的接收者
:由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。这种方式就十分接近于其他语言中面向对象中的this或者self
-
区分:两者的区分就是 接收者的类型是不是指针类型。不是指针类型就值值类型接收者
-
什么情况使用指针类型接收者
- 需要修改接收者中的值
- 接收者是拷贝代价比较大的大对象
- 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。
-
我觉得应该尽量使用指针接收者,使用起来和python 类似。属性可以看成是类属性。
给任意类型添加方法
- 接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。 举个例子,我们基于内置的int类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法
非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法
比如 不能给 int 增加方法,只能给当前main包里面的类型添加方法
//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
}
匿名字段
- 结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段。
- 会自动把类型当成名称 输入和索引的时候 使用类型名当变量名称使用
- 缺点 一个类型只能有一个,限制比较大,适用范围小。
//Person 结构体Person类型
type Person struct {
string
int
}
func main() {
p1 := Person{
"张三",
18,
}
fmt.Printf("%#v\n", p1) //main.Person{string:"张三", int:18}
fmt.Println(p1.string, p1.int) //北京 18
}
嵌套结构体
- 一个结构体中可以嵌套包含另一个结构体或结构体指针。
- 在结构其上面要添加注释 不要会提示格式报警
黄色下划线
提示 注释规则
// 类型名称 注释
- 在使用的
//Address 地址结构体
type Address struct {
Province string
City string
}
//User 用户结构体
type User struct {
Name string
Gender string
Addr Address
}
func main() {
user1 := User{
Name: "张三",
Gender: "男",
Addr: Address{
Province: "北京",
City: "房山",
},
}
fmt.Printf("user1=%#v\n", user1)//user1=main.User{Name:"张三", Gender:"男", Address:main.Address{Province:"北京", City:"房山"}}
fmt.Printf("%s的所在城市是:%#v\n", user1.Name, user1.Addr.City)//张三的所在城市是:"房山"
}
嵌套匿名结构体
匿名结构体
:和匿名字段一样,就是结构体没有命名, 默认以结构体类型当作名字- 当访问结构体成员时会先在结构体中查找该字段,找不到再去匿名结构体中查找。
- 嵌套结构体内部可能存在相同的字段名。这个时候为了避免歧义需要指定具体的内嵌结构体的字段
type Address struct {
Province string
City string
CreateTime string
}
//Email 邮箱结构体
type Email struct {
Account string
CreateTime string
}
//User 用户结构体
type User struct {
Name string
Gender string
Address // 匿名结构体
Email // 匿名结构体
}
func main() {
var user3 User
user3.Name = "范闲"
user3.Gender = "男"
user3.City = "北京" //
user3.Address.CreateTime = "2001" //指定Address结构体中的CreateTime
user3.Email.CreateTime = "2002" //指定Email结构体中的CreateTime
fmt.Printf("%#v\n", user3)
//main.User{Name:"范闲", Gender:"男", Address:main.Address{Province:"", City:"北京", CreateTime:"2001"}, Email:main.Email{Account:"", CreateTime:"2002"}}
}
结构体的继承
- Go语言中使用结构体也可以实现其他编程语言中面向对象的继承。
//Animal 动物
type Animal struct {
name string
}
func (a *Animal) move() {
fmt.Printf("%s会动!\n", a.name)
}
//Dog 狗
type Dog struct {
Feet int8
*Animal //通过嵌套匿名结构体实现继承
}
func (d *Dog) wang() {
fmt.Printf("%s会汪汪汪~\n", d.name)
}
func main() {
d1 := &Dog{
Feet: 4,
Animal: &Animal{ //注意嵌套的是结构体指针
name: "乐乐",
},
}
d1.wang() //乐乐会汪汪汪~
d1.move() //乐乐会动!
}
注意事项
- 结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)
结构体与json序列化
- JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。
- JSON键值对是用来保存JS对象的一种方式,键/值对组合中的键名写在前面并用双引号
""
包裹,使用冒号:
分隔,然后紧接着值;多个键值之间使用英文,
分隔。 - 序列化: 把Go语言中的结构体变量 --> json格式的字符串
- 反序列化: json格式的字符串 --> Go语言中能够识别的结构体变量
反序列化 ,要传入指针
// person 基类
type person struct {
Name string `json:"name" db:"name" ini:"name"`
Age int `json:"age"`
}
func main() {
p1 := person{
Name: "张三",
Age: 23,
}
// 序列化
b, err := json.Marshal(p1)
if err != nil {
fmt.Printf("marshal failed, err:%v", err)
return
}
fmt.Printf("%v\n", string(b))//{"name":"张三","age":23}
// 反序列化
str := `{"name":"李四","age":18}`
var p2 person
json.Unmarshal([]byte(str), &p2) // 传指针是为了能在json.Unmarshal内部修改p2的值
fmt.Printf("%#v\n", p2) //main.person{Name:"李四", Age:18}
}
结构体标签 Tag
- Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来。 Tag在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下:
key1:"value1" key2:"value2"
- 像上面例子里面的`json:“name” db:“name” ini:“name”`就是标签
- 结构体标签由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。键值对之间使用一个空格分隔。
注意事项
: 为结构体编写Tag时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在key和value之间添加空格。
//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!")
return
}
fmt.Printf("json str:%s\n", data) //json str:{"id":1,"Gender":"男"}
}