Go语言中没有类的概念,也不支持类的继承等面向对象的概念。Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。
一. 类型别名和自定义类型
1.1 自定义类型
在Go语言中有一些基本的数据类型,如string,整型,浮点型,布尔型等数据结构,Go中可以使用type关键字来自定义类型。
自定义类型是定义了一个全新的类型。我们可以基于内置的基本类型来定义,也可以通过struct来定义。
例如:
通过type关键字定义,MyInt就是一种新的类型,它具有int的特性。
type MyInt int
1.2 类型别名
类型别名是Go1.9版本新加的新功能。
类型别名规定:TypeAlias只是Type的别名,本质TypeAlias和Type是同一个类型。
type TypeAlias = Type
内置类型rune和byte就是类型的别名:
type byte = uint8
type rune = int32
1.3 自定义类型和类型别名的区别
自定义类和类型别名表面上看只有一个等号的差异,实际上自定义类型是新增了一个类型,而类型别名并没有新增类型。
下面的例子表明:a的类型为main.NewInt,表示在main包下定义的NewInt类型。b的类型为int,MyInt类型只会在代码中存在,编译完成时并不会有MyInt类型。
二.结构体
Go语言中的基础类型可以表示一些事物的基本属性,但是我们想表达一个事物的全部或者部分属性时,这时单一的基本数据类型已经无法满足需求了,Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名字为struct。
Go语言通过struct来实现面对对象。
2.1 结构体的定义
使用type和struct关键字来定义结构体,具体代码风格为:
type 类型名 struct{
字段名 字段类型
字段名 字段类型
...
}
其中:
- 类型名:标识自定义结构体名称,在同一个包内不能重复。
- 字段名:表示结构体的字段名。结构体中的字段名必须唯一。
- 字段类型:表示结构体字段的具体类型。
下面定义了Person和Person1自定义类型,它们拥有name,city和age三个字段,分别表示姓名,城市和年龄。这样我们使用这个Person和Person1结构体就能够很方便的在程序中存储人的信息了。
package main
type Person struct {
name string
city string
age uint8
}
// 同样类型的可以写在一行
type Person1 struct {
name, city string
age uint8
}
2.2 结构体的实例化
只有当结构体实例化时,才会正真的分配内存。也就是必须实例化后才能使用结构体字段。
结构体本身也是一种类型,我们可以像声明内置类型一样使用var关键字声明结构体类型。
var 结构体实例 结构体类型
- 基本实例化
我们可以通过'.'来访问结构体字段(成员变量),例如user1.name和user1.age
package main
import "fmt"
type Person struct {
name string
city string
age uint8
}
func main() {
var user1 Person
user1.name = "张三"
user1.city = "上海"
user1.age = 26
fmt.Printf("user1 : %v\n", user1)
fmt.Printf("user1 : %#v\n", user1)
}
- 匿名结构体
在定义一些临时数据结构等场景下还可以使用匿名结构体。
package main
import "fmt"
func main() {
var user struct {
name string
age uint8
}
user.name = "张三"
user.age = 26
fmt.Printf("user : %#v\n", user)
}
2.3 结构体指针
- 使用new关键字
我们可以通过new关键字对结构体进行实例化,得到的是结构体地址。
需要注意的是在Go语言中支持对结构体指针直接使用"."来访问结构体成员。这是Go语言的语法糖,实际底层是(*user).name = "开发"。不像C/C++需要使用"->"。
package main
import "fmt"
type Person struct {
name string
city string
age uint8
}
func main() {
var user = new(Person)
fmt.Printf("%T\n", user)
fmt.Printf("user : %#v\n", user)
//可以使用符号.来访问
user.name = "开发"
user.city = "上海"
user.age = 20
fmt.Printf("user : %#v\n", user)
}
- 取结构体地址实例化
使用&对结构体进行取地址操作相当于对结构体类型进行一次new实例化操作。
package main
import "fmt"
type Person struct {
name string
city string
age uint8
}
func main() {
user := &Person{} //相当于new(Person)
fmt.Printf("%T\n", user)
fmt.Printf("user : %#v\n", user)
user.name = "张三"
user.city = "上海"
user.age = 20
fmt.Printf("user:%#v\n", user)
}
2.4 结构体初始化
- 不进行初始化,属性为默认值
- 使用键值对进行初始化
变量:
也可以对指针进行初始化:
- 使用值的列表初始化
初始化结构体时可以简写,也就是初始化的时候不写键,直接写值,但是需要注意:
- 必须初始化结构体的所有字段
- 初始值的填充顺序必须与字段结构体中的声明顺序一样
- 该方式不能和键值初始化方式混用
2.5 结构体内存布局
结构体变量里的变量在内存中也是连续保存的。
但是也存在内存对齐,对齐规则和C语言一样:结构体在内存中的存储(内存对齐)_结构体在内存中的存放-CSDN博客
2.6 构造函数
Go语言的结构体没有构造函数,但是可以自己实现。例如:下方代码就实现了Person的构造函数。因为struct是值类型,如果结构体比较复杂,值拷贝性能开销会比较大,所以构造函数返回的是结构体指针类型。
package main
import "fmt"
type Person struct {
name string
city string
age uint8
}
func newPerson(name, city string, age uint8) *Person {
return &Person{
name: name,
city: city,
age: age,
} //&底层也是使用的new,然后再赋值
}
func main() {
user := newPerson("张三", "上海", 20)
fmt.Printf("user:%#v\n", user)
}
2.7 方法和接收者
Go语言的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)。接收者的概念类似于其他语言中的this或者self。
格式:
func (接收者变量 接收者类型) 方法名 (参数列表) 返回参数{
函数体
}
- 接收者变量:接收者的参数变量名在命名时,官方建议使用接收者类型名的第一个小写字母,而不是self,this之类的命名。例如Person类型的接收者变量应该命名为p,Connector类型的接收者变量应该命名为c等。
- 接收者类型:接收者类型和参数类似,可以是指针类型或非指针类型
- 方法名,参数列表,返回参数:具体格式与函数定义相同。
举个例子:
Dream是结构体Person中的方法,可以通过接收者变量访问结构中中的变量。
方法和函数的区别:函数不属于任何类型,方法属于特定类型。
- 指针类型的接收者
指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改是有效的。这种方式就十分接近其他语言中面向对象的this或者self。
例如:我们为Person添加一个SetAge方法,来修改实际变量年龄。
- 值类型接收者
当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但是修改只是针对副本,无法修改接收者变量本身。
什么时候使用指针类型接收者?
- 需要修改接收者中的值
- 接收者的拷贝代价比较大的大对象
- 保证一致性,如果有某个方法使用指针接收者,那么其他方法也应该使用指针接收者。
- 任意类型添加方法
在Go语言中,接收者类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。
举个例子:我们基于内置的int类型使用type关键字可以定义新的自定义类型,然后我们为自定义类型添加方法。
注意事项:非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法。
所以上面的int我们需要自定义类型为MyInt到当前包。
2.8 结构体的匿名字段
结构体允许其成员字段在声明时没有字段名,而只有类型,这种没有名字的字段就称为匿名字段。
匿名字段默认使用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同类型的匿名字段只能有一个。
2.9 嵌套结构体
一个结构体中可以嵌套包含另一个结构体或结构体指针:
- 嵌套匿名结构体
当访问结构体成员时会先在结构体中查找该字段,找不到再去匿名结构体中查找。
- 嵌套结构体内部可能存在相同的字段名
这个时候为了避免歧义需要指定具体的内嵌结构体的字段。
2.10 结构体中的继承
Go语言中使用结构体也可以实现其他编程语言中的面向对象的继承。
实际是通过嵌套实现的。
2.11 结构体字段的可见性
结构体中字段大写开头表示可以公开访问,小写表示私有(仅在定义当前结构体的包中可以访问)。
2.12 结构体于JSON序列化
JSON是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。JSON键值对是用来保存JS对象的一种方式,键/值对组合中的键名写在前面并用双引号""包裹,使用冒号:分隔,然后紧接着值; 多个键值之间使用英文,分隔。
注意结构体的变量名需要首字母大写才能被序列化和反序列化。
package main
import (
"encoding/json"
"fmt"
)
type Student struct {
ID int `json:"ID"`
Gender string `json:"Gender"`
Name string `json:"Name"`
}
type Class struct {
//变量首字母大写才能被解析
Title string //不填json键默认变量名
Stu []*Student `json:"stu"` //序列化后的数据字段是返回指定格式的,可以小写,或随意命名
}
func main() {
c := Class{
Title: "101",
Stu: make([]*Student, 0, 200),
}
for i := 0; i < 10; i++ {
stu := &Student{
ID: i,
Gender: "男",
Name: fmt.Sprintf("stu%02d", i),
}
c.Stu = append(c.Stu, stu)
}
// for i := 0; i < len(c.Stu); i++ {
// fmt.Println(c.Stu[i])
// }
//json序列化:结构体——>JSON格式字符串
data, err := json.Marshal(c)
if err != nil {
fmt.Println("json marshal fail", err)
return
}
fmt.Printf("json %s\n", data)
//json反序列化 键需要和结构体的键对应
str := `{"title":"101","stu":[{"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 fail", err)
return
}
fmt.Printf("%#v\n", c1)
}
2.13 结构体标签
Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来。
Tag在结构体字段的后方定义,由一对反引号包裹起来,具体格式如下:
`key1:"value1" key2:"value2"`
结构体标签由一个或者多个键值对组成。键与值使用冒号分隔,值用双引号括起来。键值对之间使用一个空格分隔。
注意事项:为结构体编写Tag时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行都不会有任何提示,通过反射也不会取到正确的值。例如:不要在key和value之间添加空格。
例如:我们为Student结构体的每个字段定义json序列化时使用的Tag: