1. 类型别名与自定义类型
1.1 自定义类型
在Go语言中有一些基本的数据类型,整型
、string
、浮点型
、布尔
等数据类型。
在Go语言中可以根据自身的需求,使用关键字type
自定义数据类型。
自定义类型是定义了一个全新的类型
- 我们可以根据内置的基本类型来定义。
- 也可以通过
struct
来定义。
// 基于基本类型定义自定义类型的格式
type MyInt int
通过type
关键字定义,MyInt
就是一种新的类型,它具有int
的特性
1.2 类型别名
类型别名规定:TypeAlias
是Type
的别名,本质上二者没有区别。就像读书的时候给班主任起的外号,班主任自己的名字与他的外号都指向班主任本人。
type TypeAlias = Type
我们之间学习的rune
与byte
就是类型别名
type byte = uint8
type rune = int32
1.3 类型定义和类型别名的区别
用下面这段代码就可以很清晰了解二者之间的区别
func main() {
// 自定义类型
type MyInt int
// 类型别名
type AliasInt = int
var a MyInt
var b AliasInt
fmt.Printf("%T", a)//main.MyInt
fmt.Printf("%T", b)//int
}
结果显示a的类型是main.MyInt
,表示main包下定义的MyInt
类型。b是int
类型。
AliasInt
类型只会在代码中存在,编译完成时并不会有AliasInt
类型
2.结构体
1.Go语言中基本的数据类型只能表示一些事物单一的基本属性,但是我们需要表达一个事物多个或者全部的属性时,单一的基本数据类型就无法满足需求。
2.Go语言提供了自定义数据类型的方法,可以将多个基本数据属性封装在一起,这种数据类型就是结构体。
3.类似python中的类,Go语言中通过struct
来实现面向对象
2.1 结构体的定义
使用type
与struct
关键字来定义结构体。
type 类型名 struct {
字段名 字段类型
字段名 字段类型
...
}
注意:
- 类型名:自定义结构体的名称,在同一个包内不能重名
- 字段名:结构体内部字段名称,在同一个结构体中也不能重名
- 字段类型:结构体字段的具体类型
举个栗子,定义一个人(person
) 的结构体
type person struct{
name string
age int8
addr string
}
// 类型相同的字段也可以写在一行
type person2 struct {
name,addr string
age int8
}
我们自定义了人(person)的数据类型,这个类型拥有name
,age
,addr
三个字段,分别表示人的姓名,年龄,地址。这样我们就可以通过person
结构体很方便的表示人的信息了。
内置的数据类型用来描述一个值,结构体是用来描述一组值
2.2 结构体实例化
只有当结构体实例化的时候,才被正真地分配了内存,换句话说,只有实例化后才能使用结构体的字段
结构体本质上也是一种数据类型,我们可以想声明内置类型一样,使用var
关键字声明结构体类型
2.2.1 基本实例化
package main
import "fmt"
type person struct {
name string
age int8
addr string
}
func main() {
// 实例化person类型,得到p1实例
var p1 person
p1.name = "spl"
p1.age = 18
p1.addr = "成都"
fmt.Println(p1) // {spl 18 成都}
fmt.Printf("我叫%s,今年%d岁,来自%s",p1.name,p1.age,p1.addr)
//我叫spl,今年18岁,来自成都
2.2.2 匿名结构体
当我们临时需要数据结构的场景下,可以定义匿名的结构体
func main() {
var p1 struct {
name string
age int8
addr string
}
p1.name = "spl"
p1.age = 18
p1.addr = "成都"
fmt.Println(p1) // {spl 18 成都}
fmt.Printf("我叫%s,今年%d岁,来自%s",p1.name,p1.age,p1.addr)
//我叫spl,今年18岁,来自成都
}
2.2.3 创建指针类型结构体
我们可以通过使用关键字new
对结构体实例化,得到的是结构体的内存地址
var p2 = new(person)
fmt.Printf("%v\n",p2)//&main.person{name:"spl", age:0, addr:""}
fmt.Printf("%T\n",p2)//*main.person
- p2 是一个结构体指针(指针的本质就是内存地址)
- Go语言中支持对结构体指针直接使用
.
来访问结构体的成员
var p2 = new(person)
p2.name = "spl"
p2.age = 18
p2.addr = "成都"
fmt.Printf("%#v\n",p2)
fmt.Printf("%T\n",p2)
fmt.Printf("我叫%s,今年%d岁,来自%s",p2.name,p2.age,p2.addr)
`
输出结果:
&main.person{name:"spl", age:18, addr:"成都"}
*main.person
我叫spl,今年18岁,来自成都
`
2.2.4 使用&
得到结构体地址的实例化
使用&
对结构体进行取地址操作相当于对该结构体类型进行了一次new
实例化操作
p2 := &person{} // 等同于var p2 = new(person)
p2.name = "spl"
p2.age = 18
p2.addr = "成都"
fmt.Printf("我叫%s,今年%d岁,来自%s",p2.name,p2.age,p2.addr)
值得注意的是p2.name = "spl"
的底层是 (*p2).name = "spl"
,这是Go帮助我们实现的语法糖
3.结构体实例化
没有实例化的结构体,其成员变量都是其对应类型的零值
package main
import "fmt"
type person struct {
name string
age int8
addr string
}
func main() {
var p1 person
fmt.Printf("%#v",p1) // main.person{name:"", age:0, addr:""}
}
3.1 使用键值对初始化结构体
使用键值对初始化结构体时,键对应结构体的字段,值对应该字段的初始值
p2 := person{
name: "spl",
age: 18,
addr: "成都",
}
fmt.Println(p2)
对结构体指针进行初始化
p3 := &person{
name:"spl",
age:18,
addr:"cd",
}
当结构体某一些字段没有进行初始化,可以不写该字段。此时,没有进行初始化的字段默认为该字段的零值
p3 := person{
name: "spl",
}
fmt.Printf("%#v",p3) // main.person{name:"spl", age:0, addr:""}
3.2 使用值的列表进行初始化
初始化结构体的时候可以进行简写,就是不写键,直接写值
p4 := person{
"spl",
18,
"cd",
}
fmt.Printf("%#v", p4)
使用这种格式初始化结构体的时候应该特别注意:
- 必须初始化结构体的所有字段
- 初始化的写值的顺序必须与结构体中声明的顺序一致
- 该方法不能与键值初始化进行混用
4.结构体的内存布局
结构体是占用一块连续的内存
type test struct{
a int8
b int8
c int8
d int8
}
t1 := test{
a:1,
b:2,
c:3,
d:4,
}
fmt.Printf("%p\n",&t1.a)
fmt.Printf("%p\n",&t1.b)
fmt.Printf("%p\n",&t1.c)
fmt.Printf("%p\n",&t1.d)
`0xc000012098
0xc000012099
0xc00001209a
0xc00001209b`
4.1 空结构体
空结构体不占用内存
var v struct{}
fmt.Println(unsafe.Sizeof(v)) // 0
5.阶段思考
请问下面代码的执行结果是什么?
type student struct {
name string
age int
}
func main() {
m := make(map[string]*student)
stus := []student{
{name: "小王子", age: 18},
{name: "娜扎", age: 23},
{name: "大王八", age: 9000},
}
for _, stu := range stus {
m[stu.name] = &stu
}
for k, v := range m {
fmt.Println(k, "=>", v.name)
}
}
6.构造函数
值得注意的是Go语言中没有构造函数,但是我们可以自己实现构造函数。
构造函数的实现原理就是:
- 定义一个函数
- 函数的形参是结构体的字段,返回值为结构体类型或者是结构体指针类型。
- 思考,什么时候返回结构体类型?什么时候返回结构体指针类型?
(当结构体比较复杂,返回结构体类型,开销比较大,所以返回结构体指针类型)
type person struct {
name string
age int8
}
func newPerson(name string, age int8) person {
return person{
name: name,
age: age,
}
}
func main() {
p1 := newPerson("spl", 18)
p2 := newPerson("cxm", 18)
fmt.Println(p1)
fmt.Println(p2)
7.给结构体定义方法
之前介绍的都是事物的静态字段,比如实例化的人,有名字,有年龄。但是这个实例是静态的。如何将实例动起来,比如说实例化出的人可以跑、可以跳这个应该怎么实现?
Go语言中提供了方法
的概念将实例给动起来。Go语言中的方法
本质就是一个特殊函数,这个特殊的函数与平时定义的函数有什么区别呢?
Go语言中结构体与他的方法是隔离开的(下面见到代码就能很清楚这一点),因为结构体与方法函数分离,那么怎么判断这个方法是这个结构体的呢?
Go语言应用了接受者
这个概念,通俗的讲就是给方法函数打一个标记,这个标记指向对应的结构体
方法函数的定义格式:
func (接受者变量 接受者类型) 方法函数名(参数)(返回值){
函数体
}
- 接受者变量:就是自定一个变量,方便在函数体中调用
- 接受者类型:我们要知道结构体的本质就是自定义的类型,这里的接受者就是结构体名
- 方法函数名、参数、返回值:与函数定义相同
举个栗子:
package main
import "fmt"
// 结构体
type person struct{
name string
age int8
}
// 构造函数
func newPerson(name string,age int8) person{
return person{
name: name,
age: age,
}
}
// 结构体person的方法
func (p person) Dream(addr string){
fmt.Printf("%s在%d岁的时候在%s做着靠Go语言发家致富的梦",p.name,p.age,addr)
}
func main(){
p1 := newPerson("spl",18)
p1.Dream("成都")
}
7.1 指针类型的接受者与值类型接受者
指针类型的接受者是由结构体的指针组成,由于指针的特性,调用方法时直接修改结构体实例在内存中的变量值,所以修改完成后依旧有效!
举个栗子:为person添加一个定义年龄的方法函数
1.结构体类型是结构体指针
// 结构体person的设置年龄的方法
func (p *person) SetAge(age int8){
fmt.Printf("%d岁年龄有误,修改年龄为%d\n",p.age,age)
p.age = age
}
func main(){
p2 := newPerson("cxm",10)
fmt.Printf("我是%s,我今年%d岁\n",p2.name,p2.age)
p2.SetAge(18)
fmt.Printf("我是%s,我今年%d岁\n",p2.name,p2.age)
`
我是cxm,我今年10岁
10岁年龄有误,修改年龄为18
我是cxm,我今年18岁
`
2.结构体类型不是结构体指针(为值类型)
// 结构体person的设置年龄的方法
func (p person) SetAge(age int8){
fmt.Printf("%d岁年龄有误,修改年龄为%d\n",p.age,age)
p.age = age
}
func main(){
p2 := newPerson("cxm",10)
fmt.Printf("我是%s,我今年%d岁\n",p2.name,p2.age)
p2.SetAge(18)
fmt.Printf("我是%s,我今年%d岁\n",p2.name,p2.age)
`
我是cxm,我今年10岁
10岁年龄有误,修改年龄为18
我是cxm,我今年10岁
`
当方法作用于值类型接受者时,Go语言会在代码运行时将接受者的值复制一份,在值类型接受者的方法中可以获取接受者的成员值,但是修改只能作用于复制过来的副本,无法修改接受者变量本身
7.2什么时候应用指针类型接受者?
1.需要修改接受者(结构体实例)中的值
2.接受者是拷贝代价比较大的结构体
3.保持一致性,如果某个方法使用了指针类型接受者,那么其他的方法也应该使用指针类型的接受者
8.任意类型添加方法
在Go语言中接受者的可以是任何类型,不仅仅是结构体(结构体是包含多个类型的特殊类型)任何类型都可以拥有方法。
举个栗子:我们基于内置的int
类型,使用type关键字重新定义一个新的类型,然后为这个类型添加一个自定义方法。
import "fmt"
// 根据int自定义一个MyInt类型
type MyInt int
// 为MyInt自定义一个SayHello的方法
func (m MyInt) Sayhello(){
fmt.Println("大家好啊!我是基于int类型定义的MyInt类型")
}
func main(){
var m1 MyInt
m1.Sayhello()
}
我们直接给int类型定义一个Sayhello2,报错,错误代码:cannot define new methods on non-local type int
。
原因是:我们只能为当前包的类型定义方法,不能给别的包定义方法
func (i int) Sayhello2(){
fmt.Println("大家好啊!我是int类型")
}
func main(){
var m1 MyInt
m1.Sayhello()
var m2 int
m2.Sayhello2()
}
`
cannot define new methods on non-local type int
`
9.结构体匿名字段
结构体匿名字段,不是结构体名称匿名,而是结构体包含的字段匿名。
package main
type person struct{
string
int
}
func main(){
p1 := person{
"spl",
18,
}
fmt.Println(p1.string)
fmt.Println(p1.int)
}
注意:这里的结构体匿名字段并不代表字段没有名字,而是才使用了使用类型作为字段名,结构体要求内部的字段名不能重名,因此一个结构体中相同类型的字段只能有一个
10.结构体嵌套
结构体嵌套就是一个结构体内部包含另外一个结构体
package main
import "fmt"
// 定义同一个地址的结构体
type address struct {
province string
city string
}
// 定义一个人的结构体
type user struct {
name string
age int8
address address
}
func main(){
u1 := user{
name: "spl",
age: 18,
address: address{
province: "四川",
city: "成都",
},
}
fmt.Println(u1.address.city)
fmt.Println(u1.address.province)
}
10.1 嵌套匿名字段
上面user结构体中的嵌套address
结构体可以采用匿名字段的方法
package main
import "fmt"
// 定义一个地址结构体
type addr struct {
provice string
city string
}
// 定义一个人结构体
type user struct {
name string
age int8
addr
}
func main() {
var u1 user
u1.name = "spl"
u1.age = 18
u1.addr.provice = "四川"// 匿名字段默认使用类型名作为字段名
u1.city = "成都" // 匿名字段也可以省略
fmt.Println(u1) //{spl 18 {四川 成都}}
}
当访问结构体成员时会现在结构体中进行查找,找不到再去嵌套的匿名字段中查找
10.2 嵌套结构体的字段名冲突
嵌套结构体内的字段,字段名可能会存在相同的字段名。在这种情况下就避免歧义,使用具体的内嵌结构体字段名。
// 定义address结构体
type address struct {
provice string
city string
}
// 定义mail 结构体
type mail struct {
provice string
city string
}
// 定义user 结构体
type user struct {
name string
age int8
address
mail
}
func main(){
var u1 user
u1.name = "spl"
u1.age = 18
u1.address.provice = "四川" // 指定address中的Provice
u1.address.city = "成都" // 指定address 中的city
u1.mail.provice = "四川" // 指定 mail 中的Provice
u1.mail.city = "彭州" // 指定 mail 中的city
}
11 结构体的继承
通过嵌套结构体匿名字段实现继承
package main
import "fmt"
// 定义一个Animal结构体
type Animal struct{}
// 给animal结构体添加一个move方法
func (a Animal) move() {
fmt.Println("动起来了!")
}
//定义一个Dog结构体,继承Animal结构体
type Dog struct{
name string
Animal
}
func main(){
d1 := Dog{
name: "布鲁托",
}
d1.move()
}
12 结构体字段的可见性
结构体中字段大写开头表示可以公开访问,小写表示私有(仅在定义的当前结构体的包内可以访问)
13 结构体与Json序列化
//Student 学生
type Student struct {
ID int
Gender string
Name string
}
//Class 班级
type Class struct {
Title string
Students []*Student
}
func main() {
c := &Class{
Title: "101",
Students: make([]*Student, 0, 200),
}
for i := 0; i < 10; i++ {
stu := &Student{
Name: fmt.Sprintf("stu%02d", i),
Gender: "男",
ID: i,
}
c.Students = append(c.Students, stu)
}
//JSON反序列化:JSON格式的字符串-->结构体
str := `{"Title":"101","Students":[{"ID":0,"Gender":"男","Name":"stu00"},{"ID":1,"Gender":"男","Name":"stu01"},{"ID":2,"Gender":"男","Name":"stu02"},{"ID":3,"Gender":"男","Name":"stu03"},{"ID":4,"Gender":"男","Name":"stu04"},{"ID":5,"Gender":"男","Name":"stu05"},{"ID":6,"Gender":"男","Name":"stu06"},{"ID":7,"Gender":"男","Name":"stu07"},{"ID":8,"Gender":"男","Name":"stu08"},{"ID":9,"Gender":"男","Name":"stu09"}]}`
c1 := &Class{}
err = json.Unmarshal([]byte(str), c1)
if err != nil {
fmt.Println("json unmarshal failed!")
return
}
fmt.Printf("%#v\n", c1)
}
14 结构体和方法补充
因为slice和map这两种数据类型都包含了指向底层数据的指针,因此我们在需要复制它们时要特别注意。我们来看下面的例子:
type Person struct {
name string
age int8
dreams []string
}
func (p *Person) SetDreams(dreams []string) {
p.dreams = dreams
}
func main() {
p1 := Person{name: "小王子", age: 18}
data := []string{"吃饭", "睡觉", "打豆豆"}
p1.SetDreams(data)
// 你真的想要修改 p1.dreams 吗?
data[1] = "不睡觉"
fmt.Println(p1.dreams) // ?
正确的做法是在方法中使用传入的slice的拷贝进行结构体赋值。
func (p *Person) SetDreams(dreams []string) {
p.dreams = make([]string, len(dreams))
copy(p.dreams, dreams)
}
同样的问题也存在于返回值slice和map的情况,在实际编码过程中一定要注意这个问题。