类型别名和自定义类型

自定义类型

Go语言中有一些的基本的数据类型,例如string整型浮点型布尔等数据类型。

Go语言中可以使用type关键字来定义自己的类型。

自定义类型是定义了一个全新的类型。可以基于内置的基本数据类型进行定义,也可以通过struct定义。

1
2
3
4
5
6
7
//自定义类型
type myInt int

//自定义类型
var a myInt
a = 100
fmt.Printf("myInt类型是%T, 值为%d\n", a, a)//myInt类型是main.myInt, 值为100

类型别名

TypeAlias只是Type的类型名字的别称,本质上TypeAlias 的类型与 Type类型是同一个类型。

1
2
//类型别名
type TypeAlias = Type

runebyte就是类型别名

1
2
type byte = uint8
type rune = int32
1
2
3
4
5
6
7
//类型别名
type TypeAlias = int

//类型别名
var b TypeAlias
b = 100
fmt.Printf("myInt类型是%T, 值为%d\n", b, b) //myInt类型是int, 值为100

结构体:

1
2
3
4
5
type 类型名 struct {
字段名 字段类型
字段名 字段类型
...
}

其中:

  • 类型名:标识自定义结构体的名称,在同一个包内不能重复。

  • 字段名:标识结构体字段名。同一个结构体中的字段名必须唯一。

  • 字段类型:表示结构体字段的具体类型。

1
2
3
4
5
6
7
//定义结构体
type person struct {
name string
age int
gender string
hobby []string
}

1
2
3
4
5
//相同类型的字段可以写在同一行
type animal struct {
name, city string
age int
}
1
2
3
4
5
6
7
8
//Go 语言中如果使用的标识符首字母是大写的,就表示对外部包可见(暴露,共有的)
//并且对该注释格式有要求

// Dog 这是一个对外可见结构体的注释
type Dog struct{
name string
age int
}

*struct是值类型*

1
2
3
4
5
6
//struct 是值类型
p5 := person{"方式五", "五", 5}
fmt.Println(p5) //{方式五 五 5}
p6 := p5
p6.street = "我是copy"
fmt.Println(p5) //{方式五 五 5}

结构体实例化

1
var 结构体实例 结构体类型

基本实例化

1
2
3
4
5
6
7
8
9
10
//声明一个person类型的变量p
var p person
//通过字段进行赋值
p.name = "八戒"
p.age = 18
p.gender = "男"
p.hobby = []string{"篮球","足球","双色球"}
fmt.Printf("type: %T, value%v\n", p, p) //type: main.person, value{八戒 18 男 [篮球 足球 双色球]}
//访问变量的字段
fmt.Println(p.name) //八戒

匿名结构体

1
2
3
4
5
6
7
8
//匿名结构体
var a struct{
name string
age int
}
a.name = "李四"
a.age = 23
fmt.Printf("type: %T, value%v\n", a, a) //type: struct { name string; age int }, value{李四 23}

创建指针类型结构体

Go语言支持对结构体指针直接使用.来访问结构体的成员变量

1
2
3
4
5
//使用`new`关键字对结构体进行实例化,得到的是结构体的地址。
var p = new(person)
fmt.Printf("%T\n", p) //*main.person
p.name = "张三"
fmt.Printf("%#v", p) //&main.person{name:"", street:"", age:0}

取结构体的地址实例化

使用&对结构体进行取地址操作相当于对该结构题进行一次new实例化操作

语法糖,自动根据内存地址找到那个原变量。

1
2
3
4
5
6
//方式一
var p1 = new(person)
p1.name = "方式一"
p1.age = 1
p1.street = "一"
fmt.Printf("%v\n",p1)

结构体初始化

没有初始化的结构体,其成员变量对应的其类型的零值

1
2
var p person
fmt.Printf("%#v\n",p) //main.person{name:"", street:"", age:0}

使用键值对初始化

1
2
3
4
5
6
7
//方式三 键值对初始化
p3 := person{
name : "方式三",
street: "三",
age : 3,
}
fmt.Printf("%#v\n",p3) //main.person{name:"方式三", street:"", age:3}

也可以对其结构体指针初始化

1
2
3
4
5
6
p3_1 := &person{
name : "方式三_一",
street: "三——一",
age : 31,
}
fmt.Printf("%#v\n",p3_1) //&main.person{name:"方式三_一", street:"三——一", age:31}

某些字段无初始值可以不写

1
2
3
4
5
p3_2 := &person{
name : "方式三_二",
age : 32,
}
fmt.Printf("%#v\n",p3_2) //&main.person{name:"方式三_二", street:"", age:32}

使用值列表初始化

1
2
3
4
5
6
7
//方式四
p4 := person{
"方式四",
"四",
4,
}
fmt.Printf("%#v\n",p4) //main.person{name:"方式四", street:"四", age:4}

注意:

  • 1.必须初始化结构体所有字段。
  • 2.初始值的时候填充顺序与字段在结构体中声明的顺序一致。
  • 3.该方式不能与键值初始化方式混用。

结构体内存布局

结构体占用一块连续的内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type v struct{
a int8
b int8
c int8
d int8
}

func f() {
n := v{1,2,3,4}
fmt.Println(&(n.a)) //0xc00000a190
fmt.Println(&(n.b)) //0xc00000a191
fmt.Println(&(n.c)) //0xc00000a192
fmt.Println(&(n.d)) //0xc00000a193
}

【进阶知识点】关于Go语言中的内存对齐推荐阅读:在 Go 中恰到好处的内存对齐

空结构体

空结构体不会占用内存空间。

1
2
3
4
//空结构体不会占用内存空间
var v struct{}
fmt.Println(unsafe.Sizeof(v))//空结构体不会占用内存空间
type null_struct struct{}

构造函数

Go语言没有内置的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type person struct {
name string
age int
}

//构造函数:约定俗成用new开头
//返回的是结构体还是结构体指针
//当结构体比较大时尽量使用结构体指针,减少程序内存的开销
func newPerson(name string, age int) *person {
return &person{
name: name,
age: age,
}
}

func main() {
//此处返回的是指针类型
p := newPerson("张三", 23)
fmt.Println(p)
}

注意:

  • 构造函数:约定俗成用new开头

  • 返回的是结构体还是结构体指针

  • 当结构体比较大时尽量使用结构体指针,减少程序内存的开销

方法和接收者

Go语言中的方法(Method)是一种作用于特定类型类型变量的函数。这种特定类型变量叫做接收者(Receiver)。接收者的概念就类似于其它语言中的thisself

1
2
3
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数){
函数体
}

其中:

  • 接收者变量:接收者中的参数在命名时,官方建议使用接受者类型名称首字母小写,而不是selfthis之类的命名。
  • 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
  • 方法名、参数列表、返回参数:具体格式于函数定义相同。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Function,任何人都能调用
func speak(p person){
fmt.Println(p.language)
}

// Method方法,只属于结构体person的方法
func (p person) talk() {
fmt.Println(p.language)
}

func main() {
p := person{"张三", 18, "Chinese"}
//函数使用
speak(p)

//方法使用
p.talk()
}

值类型接收者和指针类型接收者

指针类型的接受者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改一眼是有效的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//值接收者,改变的只是副本的值,本质还是结构体是值类型
func (p person) newYear(){
p.age++
}

//指针接收者,需改变对象是时使用指针接收者
func (p *person) realNewYear() {
p.age++
}

func main() {
p := person{"张三", 18, "Chinese"}
//普通方法使用
speak(p)

//结构体方法使用
p.talk()

fmt.Println(p.age) //18
p.newYear()
fmt.Println(p.age) //18
p.realNewYear()
fmt.Println(p.age) //19

}

什么时候应该使用指针类型接收者

  • 1、需要修改接收者中的值
  • 2、接收者是拷贝代价比较大的对象
  • 3、保证一致性,如果某个方法使用了指针接收者,那么其他方法也应该使用指针接收者。

任意类型添加方法

在Go语言中,接收者可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。

包括最基础的数据类型,但是要使用自定义类型,再对其添加方法。

不能给别的包里面的类型添加方法,只能给自己的包里面的类型添加方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 自定义类型添加方法
// 例如不能给基本类型添加方法,只能通过自定义类型 type myInt int
// 不能给别的包里面的类型添加方法,只能给自己的包里面的类型添加方法

type myInt int

func (m myInt) hello(){
fmt.Println("我是一个int")
}

func main() {
m := myInt(100)
m.hello() // 我是一个int
}

结构体匿名字段

结构体的其成员字段在声明时没有名字,只有类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//匿名字段,同类型只能有一个
type person struct {
string
int
}

func main() {
//匿名字段初始化,直接按定义时的类型初始化
var p = person{
"张小三",
18,
}

//匿名字段的使用,直接对象.类型进行使用
//获取name
fmt.Println(p.string)
//获取年龄
fmt.Println(p.int)
}

匿名字段默认采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中的同类型的你们那个字段只能有一个。

嵌套结构体

一个结构体内可以包含另外一个结构体或者结构体指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//人
type person struct{
name string
age int
addr address
}

//公司
type company struct {
name string
addr address
}

//地址
type address struct{
province string
city string
}

func main() {
//嵌套结构体初始化
var c = company{
"字节跳动",
address{
"云南",
"昆明",
},
}
fmt.Println(c)

var p = person{
"张三",
18,
address{
"重庆",
"渝北",
},
}
fmt.Println(p)

匿名嵌套结构体

匿名嵌套结构体的好处可以直接.内部嵌套的结构体的字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//人
type person struct{
name string
age int
//匿名字段
address
}

//地址
type address struct{
province string
city string
}

func main() {
//匿名嵌套结构体的初始化
p := person{
name: "张三",
age: 18,
//匿名嵌套的结构体直接使用该类型进行初始化
address: address{
province: "成都",
city: "锦里",
},
}
//匿名嵌套结构体的有点,不用.嵌套的结构体就可以拿到其字段,规则是先使用父级的,一次往里使用
fmt.Println(p.city)

匿名嵌套结构体的字段冲突

嵌套结构体内可能存在相同的字段名,避免歧义就需指定具体嵌套的结构体的类型及字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//pig
type pig struct {
name string
address
house
}

//地址
type address struct{
province string
city string
}

//房子
type house struct {
city string
price int
}

func main() {
//匿名嵌套结构体的初始化
pig1 := pig{
name: "佩奇",
address: address{
"重庆",
"江北",
},
house: house{
"大理",
8000,
},
}
//匿名嵌套结构体字段名冲突时需指定具体的结构体类型
fmt.Println(pig1.address.city)
fmt.Println(pig1.address.city)
}

结构体的”继承“

Go语言可以使用结构体,,模拟其他编程语言中的面向对象的继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//动物
type animal struct {
name string
}

func (a animal) move(){
fmt.Printf("%s会动!\n", a.name)
}

//狗
type dog struct {
feet uint8
animal
}

func (d dog) bark(){
fmt.Printf("%s在叫:汪汪汪!!!\n", d.name)
}

func main() {
d1 := dog{
feet: 4,
animal: animal{
name:"佩奇",
},
}
d1.bark()
d1.move()
}

结构体字段的可见性

结构体中小写表示私有,仅在此类中中可以使用,对其他类不可见。大写表示公开,在其他包可以直接使用,包含方法,属性等。

结构体与JSON

1
2
3
4
5
6
type person struct {
//`json:"name" db:"name" ini:"name"`该处表示转json时用的名字,数据库字段名,ini文件字段名
//属性名小写开头表示私有,大写表示公有,外部可见
Name string `json:"name" db:"name" ini:"name"`
Age int `json:"age"`
}

序列化

1
2
3
4
5
6
7
8
9
10
p := person{
Name: "张三",
Age: 18,
}
//序列化
var pstr,err = json.Marshal(p)
if err != nil {
fmt.Printf("序列化出错:%s", err)
}
fmt.Println(string(pstr))

反序列化

1
2
3
4
5
6
p2str := `{"name":"李四","age":45}`
var p2 person
//反序列化
//p2传地址因为对象是值传递,需要修改对象必须传地址
json.Unmarshal([]byte(p2str), &p2)
fmt.Println(p2)

结构体标签(Tag)

Tag是结构体的元信息,可以在运行的时候通过反射机制读取出来。Tag在结构体的后方定义,由一对反引号包裹起来。

1
`key1:"value" key2:"value"`

同一个结构体字段可以设置多个键值对标签Tag

注意事项:

​ 为结构体编写Tag时,必须严格遵守键值对规则,结构体标签的解析代码的容错能力很弱,一旦格式写错,编译和运行是不会提示任何错误,通过反射也无法正确取值。例如不要在key与value之间加空格。

1
2
3
4
5
6
type person struct {
//`json:"name" db:"name" ini:"name"`该处表示转json时用的名字,数据库字段名,ini文件字段名
//属性名小写开头表示私有,大写表示公有,外部可见
Name string `json:"name" db:"name" ini:"name"`
Age int `json:"age"`
}

结构体和方法知识点补充

slice和map这两种数据类型都包含了指向底层数组的指针,我们在对其进行复制时要注意复制拷贝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//struct补充
type Person struct {
name string
age int
dreams []string
}

//slice和map这两种数据类型指向底层数组的指针,再复制他们时要特别注意。
func (p *Person) SetDreams(dreams []string) {
p.dreams = dreams
}

//正确做法
func (p *Person) SetDreamsReal(dreams []string){
p.dreams = make([]string, len(dreams))
copy(p.dreams, dreams)
}

func main() {
p1 := Person{name:"小王子",age:18}
data := []string{"吃饭","睡觉","打豆豆"}
p1.SetDreams(data)
fmt.Println(p1) //{小王子 18 [吃饭 睡觉 打豆豆]}
data[1] = "不睡觉"
fmt.Println(p1) //{小王子 18 [吃饭 不睡觉 打豆豆]}

p1.SetDreamsReal(data)
fmt.Println(p1) //{小王子 18 [吃饭 不睡觉 打豆豆]}
data[1] = "学习"
fmt.Println(p1) //{小王子 18 [吃饭 不睡觉 打豆豆]}

}