Go 结构体
类型别名&定制
类型别名
类型别名是Go
的1.9版本中新添加的功能。
大概意思就是给一个类型取一个别名,小名等,但是这个别名还是指向的相同类型。
如uint32
的别名rune
,其底层还是uint32
。
如uint8
的别名byte
,使用byte
实际上还是uint8
别名的作用在于在编程中更方便的进行使用类型,如下示例,我们为int64
取一个别名long
。
package main
import (
"fmt"
)
func main() {
type long = int64
var num long
num = 100000000
fmt.Printf("%T %v", num, num)
// int64 100000000
}
自定类型
自定类型类似于继承某个内置的类型,它会以一种全新的类型出现,并且我们可以为该自定类型做一些定制方法。
如下定义的small
类型,是基于uint8
的一个类型。它是一种全新的类型,但是具有uint8
的特性。
package main
import (
"fmt"
)
func main() {
type small uint8
var num small = 32
fmt.Printf("%T %v", num, num)
// main.small 32
}
区别差异
可以看到上面示例中的打印结果
// int64 100000000
// main.small 32
结果显示自定义类型是main.small
,其实自定义类型只在代码中存在,编译时会将其转换为uint8
。
结构体
结构体类似于其他语言中的面向对象,值得一提的是Go
语言是一种面向接口的语言,所以弱化了对面向对象方面的处理。
在结构体中,我们可以清晰的表示一个现实中的事物,注意:结构体其实就是一种自定义的类型。
在Go
中使用struct
来定义结构体。
以下是语法介绍,type
和struct
这两个关键字用于定义结构体。
type 类型名 struct {
字段名 字段类型
字段名 字段类型
…
}
类型名:标识自定义结构体的名称,在同一个包内不能重复。如果不是对外开放的接口,则首字母小写,否则大写。
字段名:表示结构体字段名。结构体中的字段名必须唯一。
字段类型:表示结构体字段的具体类型。
下面我们来定义一个dog
的结构体。
// 命名小写,表示dog结构体不对外开放
type dog struct{
dogName string
dogAge int8
dogGender bool
}
.实例化
当结构体定义完成后,必须对其进行实例化后才可使用。
单纯的定义结构体是不会分配内存的。
以下将介绍通过.
进行实例化。
基本实例化
下面是基本实例化的示例。
首先定义一个变量,声明它是dog
类型,再通过.
对其中的字段进行赋值。
package main
import (
"fmt"
)
type dog struct{
dogName string
dogAge int8
dogGender bool
}
func main() {
var d1 dog
d1.dogName = "大黄"
d1.dogAge = 12
d1.dogGender = true
fmt.Println(d1)
// {大黄 12 true}
}
匿名结构体
有的结构体只使用一次,那么就可以使用匿名结构体在定义之初对其进行实例化。
这个时候只使用stuct
即可,不必使用type
进行类型的自定义。
package main
import (
"fmt"
)
func main() {
var dog struct{
dogName string
dogAge int8
dogGender bool
}
dog.dogName = "大黄"
dog.dogAge = 12
dog.dogGender = true
fmt.Println(dog)
// {大黄 12 true}
}
指针实例化
通过new
可以对结构体进行实例化,具体步骤是拿到其结构体指针后通过.
对其字段填充,进而达到实例化的目的。
package main
import (
"fmt"
)
type dog struct{
dogName string
dogAge int8
dogGender bool
}
func main() {
var d1 = new(dog) // 拿到结构体指针
d1.dogName = "大黄"
d1.dogAge = 12
d1.dogGender = true
fmt.Println(d1)
// &{大黄 12 true}
}
地址实例化
使用&
对结构体进行取地址操作相当于对该结构体类型进行了一次new
实例化操作。
与上面的方式本质都是相同的。
d1.dogName= "大黄"
其实在底层是(*d1).dogName= "大黄"
,这是Go
语言帮我们实现的语法糖。
package main
import (
"fmt"
)
type dog struct{
dogName string
dogAge int8
dogGender bool
}
func main() {
d1 := &dog{}
d1.dogName = "大黄"
d1.dogAge = 12
d1.dogGender = true
fmt.Println(d1)
// &{大黄 12 true}
}
{}实例化
实例化时,除开可以使用.
也可以使用{}
。
在实际开发中使用{}
实例化的普遍更多。
基本实例化
以下是使用{}
进行基本实例化的示例。
key
对应字段名,value
对应实例化的填充值。
package main
import (
"fmt"
)
type dog struct{
dogName string
dogAge int8
dogGender bool
}
func main() {
d1 := dog{
dogName:"大黄",
dogAge:12,
dogGender:true,
}
fmt.Print(d1)
}
顺序实例化
可以不填入key
对其进行实例化,但是要与定义结构体时的字段位置一一对应。
- 必须初始化结构体的所有字段。
- 初始值的填充顺序必须与字段在结构体中的声明顺序一致。
- 该方式不能和键值初始化方式混用。
package main
import (
"fmt"
)
type dog struct{
dogName string
dogAge int8
dogGender bool
}
func main() {
d1 := dog{
"大黄",
12,
true,
}
fmt.Print(d1)
}
地址实例化
下面是使用{}
进行地址实例化。
package main
import (
"fmt"
)
type dog struct{
dogName string
dogAge int8
dogGender bool
}
func main() {
d1 := &dog{
"大黄",
12,
true,
}
fmt.Print(d1)
}
内存布局
连续内存
一个结构体中的字段,都是占据一整块连续内存。
但有的字段可能看起来不会与前一个字段进行相邻,这是受到类型的影响,具体可查看:在 Go 中恰到好处的内存对齐
package main
import (
"fmt"
)
type dog struct {
dogName string
dogAge int8
dogGender bool
}
func main() {
d1 := &dog{
"大黄",
12,
true,
}
fmt.Printf("%p \n", &d1.dogName)
fmt.Printf("%p \n", &d1.dogAge)
fmt.Printf("%p \n", &d1.dogGender)
// 0xc0000044a0
// 0xc0000044b0
// 0xc0000044b1
}
空结构体
一个空的结构体是不占据任何内存的。
package main
import (
"fmt"
"unsafe"
)
func main() {
var dog struct{}
fmt.Print(unsafe.Sizeof(dog)) // 0 查看占据的内存
}
构造函数
当一个函数返回一个结构体实例时,该函数将被称为构造函数。
Go
语言中没有构造函数,但是我们可以自己进行定义。
注意构造函数的命名方式要用new
进行开头。
普通构造
由于函数的参数传递都是值传递,所以每次需要将结构体拷贝到构造函数中,这是非常消耗内存的。
所以在真实开发中不应该使用这种方式
package main
import (
"fmt"
)
type dog struct {
dogName string
dogAge int8
dogGender bool
}
// dog每次都会进行拷贝,消耗内存
func newDog(dogName string, dogAge int8, dogGender bool) dog {
return dog{
dogName: dogName,
dogAge: dogAge,
dogGender: dogGender,
}
}
func main(){
d1 := newDog("大黄",12,true)
fmt.Printf("%p \n",&d1)
}
指针构造
如果让其使用指针构造,就不用每次都会进行拷贝了。推荐使用该方式。
只需要加上*
与&
即可。
package main
import (
"fmt"
)
type dog struct {
dogName string
dogAge int8
dogGender bool
}
// 每次的传递都是dog的指针,所以不会进行值拷贝
func newDog(dogName string, dogAge int8, dogGender bool) *dog {
return &dog{
dogName: dogName,
dogAge: dogAge,
dogGender: dogGender,
}
}
func main(){
d1 := newDog("大黄",12,true)
fmt.Printf("%p \n",&d1)
}
方法&接收者
为任意类型定制一个自定义方法,必须要为该类型进行接收者限制。
接收者类似于其他语言中的self
与this
。
定义方法格式如下:
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
函数体
}
接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是
self
、this
之类的命名。例如,Person
类型的接收者变量应该命名为p
,Connector
类型的接收者变量应该命名为c
等。接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
方法名、参数列表、返回参数:具体格式与函数定义相同。
普通接收者
以下示例是定义普通接收者方法。
注意,这是值拷贝,意味着你的d
会拷贝d1
的数据。
package main
import (
"fmt"
)
type dog struct {
dogName string
dogAge int8
dogGender bool
}
func newDog(dogName string, dogAge int8, dogGender bool) *dog {
return &dog{
dogName: dogName,
dogAge: dogAge,
dogGender: dogGender,
}
}
func (d dog)getAge() int8 {
return d.dogAge // 返回狗狗的年龄
}
func main(){
d1 := newDog("大黄",12,true)
age := d1.getAge()
fmt.Print(age) // 12
}
指针接收者
由于普通接收者方法无法做到修改原本实例化对象数据的需求,所以我们可以定义指针接收者方法进行引用传递。
如下,调用addAge()
方法会将原本的年龄加上十岁。
package main
import (
"fmt"
)
type dog struct {
dogName string
dogAge int8
dogGender bool
}
func newDog(dogName string, dogAge int8, dogGender bool) *dog {
return &dog{
dogName: dogName,
dogAge: dogAge,
dogGender: dogGender,
}
}
func (d *dog) addAge() {
d.dogAge += 10
}
func main() {
d1 := newDog("大黄", 12, true)
fmt.Printf("旧年龄:%v", d1.dogAge) // 12
d1.addAge()
fmt.Printf("新年龄:%v", d1.dogAge) // 22
}
关于使用d1
直接调用,这是一种语法糖形式。完整的形式应该是使用&
取到地址后再进行传递,但是这样会出现一些问题。
所以直接使用实例化对象调用即可。
下面是关于指针方法的一些使用注意事项:
- 修改原本实例化对象中的值时,应该使用指针接收者方法
- 实例化对象的内容较多,拷贝代价较大时,应该使用指针接收者方法
- 如果该对象下某个方法是指针接收者方法,那么为了保持一致性,其他方法也应该使用指针方法。
自定类型方法
下面将对自定义类型small
做一个方法,getBool
获取其布尔值。
package main
import (
"fmt"
)
type small uint8
func (s small) getBool() bool {
if s != 0 {
return true
}
return false
}
func main() {
var num small
num = 18
result := num.getBool()
fmt.Print(result) // true
}
匿名字段
基本使用
匿名字段即只使用字段类型,不使用字段名。
使用较少
注意:这里匿名字段的说法并不代表没有字段名,而是默认会采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。
package main
import (
"fmt"
)
type dog struct {
string // 只能出现一次同类型的字段。
int8
bool
}
func main() {
d1 := dog{
string: "大黄",
int8: 12,
bool: true,
}
fmt.Print(d1)
}
结构体嵌套
基本使用
一个结构体中可以嵌套另一个结构体。
通过这种方式,可以达到继承的效果。
package main
import (
"fmt"
)
type details struct {
phone string // 电话
addr string // 地址
}
type person struct {
name string
gender bool
age int8
details // 匿名字段,详细信息
}
func main() {
p1 := person{
name: "云崖",
gender: true,
age: 18,
details: details{ // 对匿名字段的嵌套结构体进行实例化
phone: "1008611",
addr: "北京市海淀区",
},
}
fmt.Print(p1)
// {云崖 true 18 {1008611 北京市海淀区}}
}
匿名简写
如果要访问上例中的电话,可以使用简写形式。也可以使用全写形式。
查找顺序是先查找具名字段,再查找匿名字段。
要注意多个结构体嵌套产生的字段名冲突问题。
package main
import (
"fmt"
)
type details struct {
phone string // 电话
addr string // 地址
}
type person struct {
name string
gender bool
age int8
details // 匿名字段,详细信息
}
func main() {
p1 := person{
name: "云崖",
gender: true,
age: 18,
details: details{ // 对匿名字段的嵌套结构体进行实例化
phone: "1008611",
addr: "北京市海淀区",
},
}
fmt.Println(p1.phone) // 简写
fmt.Println(p1.details.phone) // 全写
}
JSON
使用JSON
包可对结构体进行序列化操作。
常用于前后端数据交互。
字段可见性
由于json
包是再encoding/json
中,所以我们要想让main
包的结构体能被json
包访问,需要将结构体名字,字段名字等进行首字母大写。
结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。
基本使用
以下是关于JSON
序列化与反序列化的基本使用。
package main
import (
"encoding/json" // 导包
"fmt"
)
// Details 详情 对于大写的结构体,应该具有注释。注意空格
type Details struct {
Phone string // 电话
Addr string // 地址
}
// Person 人
type Person struct {
Name string
Gender bool
Age int8
Details // 匿名字段,详细信息
}
func main() {
p1 := Person{
Name: "云崖",
Gender: true,
Age: 18,
Details: Details{ // 对匿名字段的嵌套结构体进行实例化
Phone: "1008611",
Addr: "北京市海淀区",
},
}
// 序列化 得到一个[]bytes类型
data, err := json.Marshal(p1)
if err != nil {
fmt.Println("json error")
return
}
fmt.Println(string(data)) // 查看结果
// {"Name":"云崖","Gender":true,"Age":18,"Phone":"1008611","Addr":"北京市海淀区"}
// 反序列化
p2 := Person{}
json.Unmarshal(data, &p2) // 反序列化时需要实例化出该结构体。通过地址对其进行赋值
fmt.Println(p2) // {云崖 true 18 {1008611 北京市海淀区}}
}
标签使用
我们可以看到上面的序列化后的结果字段名都是大写名字开头的。
{"Name":"云崖","Gender":true,"Age":18,"Phone":"1008611","Addr":"北京市海淀区"}
怎么样把它转换为小写?这个需要使用到结构体标签。
示例如下:
package main
import (
"encoding/json" // 导包
"fmt"
)
// Details 详情 对于大写的结构体,应该具有注释。注意空格
type Details struct {
// 代表在json转换中,Phone更名为phone orm中更名为phone 配置文件ini中更名为phone
Phone string `json:"phone" db:"phone" ini:"phone"`
Addr string `json:"addr"`
}
// Person 人
type Person struct {
Name string `json:"name"`
Gender bool `json:"gender"`
Age int8 `json:"age"`
Details // 匿名字段,详细信息
}
func main() {
p1 := Person{
Name: "云崖",
Gender: true,
Age: 18,
Details: Details{ // 对匿名字段的嵌套结构体进行实例化
Phone: "1008611",
Addr: "北京市海淀区",
},
}
// 序列化 得到一个[]bytes类型
data, err := json.Marshal(p1)
if err != nil {
fmt.Println("json error")
return
}
fmt.Println(string(data)) // 查看结果
// {"name":"云崖","gender":true,"age":18,"phone":"1008611","addr":"北京市海淀区"}
// 反序列化
p2 := Person{}
json.Unmarshal(data, &p2) // 反序列化时需要实例化出该结构体。通过地址对其进行赋值
fmt.Println(p2) // {云崖 true 18 {1008611 北京市海淀区}}
}
小题目
使用“面向对象”的思维方式编写一个学生信息管理系统。
- 学生有id、姓名等信息
- 程序提供展示学生列表、添加学生、编辑学生信息、删除学生等功能
main.go
package main
import (
"fmt"
"os"
)
type student struct {
id int64
name string
}
func showMenu() {
fmt.Println("welcome sms!")
fmt.Println(`
1.查看所有学生
2.添加学生
3.修改学生
4.删除学生
5.退出
`)
}
func main() {
smr := studentMgr{
allStudent: make(map[int64]student, 50), // 实例化出一个map,最多容纳50组键值对
}
for {
showMenu()
// 获取输入
fmt.Print("输入您的选项>>>")
var choice int
fmt.Scanln(&choice)
switch choice {
case 1:
smr.showStudent()
case 2:
smr.addStudent()
case 3:
smr.editStudent()
case 4:
smr.deleteStudent()
case 5:
os.Exit(1) // 退出
default:
fmt.Println("输入有误...")
}
}
}
studentMgr.go
package main
import "fmt"
type studentMgr struct {
allStudent map[int64]student
// 管理者 增删改查
}
// 查
func (s studentMgr) showStudent() {
for _, stu := range s.allStudent {
fmt.Printf("学号:%d 姓名:%s \n", stu.id, stu.name)
}
}
// 增
func (s studentMgr) addStudent() {
var (
stuID int64
stuName string
)
fmt.Print("请输入学号>>>")
fmt.Scanln(&stuID)
fmt.Print("请输入姓名>>>")
fmt.Scanln(&stuName)
newStu := student{
id: stuID,
name: stuName,
}
s.allStudent[newStu.id] = newStu
}
// 改
func (s studentMgr) editStudent() {
var stuID int64
fmt.Print("请输入学号>>>")
fmt.Scanln(&stuID)
stuObj, ok := s.allStudent[stuID] // 值拷贝
if !ok {
fmt.Println("没有该学生")
return
}
fmt.Printf("学生信息如下:\n 学号:%d\n 姓名:%s\n", stuObj.id, stuObj.name)
fmt.Print("请输入学生的新名字>>>")
var newName string
fmt.Scanln(&newName)
stuObj.name = newName
s.allStudent[stuID] = stuObj
}
// 删
func (s studentMgr) deleteStudent() {
var stuID int64
fmt.Print("请输入要删除的学生学号>>>")
fmt.Scanln(&stuID)
_, ok := s.allStudent[stuID]
if !ok {
fmt.Println("没有该学生")
return
}
delete(s.allStudent, stuID)
fmt.Println("删除成功")
}
测试使用命令:
go run main.go studentMgr.go