相比于数组,结构体的应用场景就更加丰富和灵活。 结构体是一种自定义类型,也是值类型。一个结构体可以由多个基本数据类型或者结构体(不包括自己,但可以是自己的指针类型)组成。每个值称为结构体的成员。
下面以经典的公司员工信息为例子,展示结构体在实际编程中的应用。
//相邻的成员类型如果相同的话可以被合并到一行
type Employee struct {
ID int
Name, Address string
DoB time.Time
Position string
Salary int
ManagerID int
}
var dilbert Employee
//结构体变量的成员可以通过点操作符访问
dilbert.Salary -= 5000
//对成员取地址然后通过指针访问
position := &dilbert.Position
*position = "Senior " + *position
//点操作符可以和指向结构体的指针一起工作
var employeeOfTheMonth *Employee = &dilbert
employeeOfTheMonth.Position += " (proactive team player)"
结构体成员的输入顺序也有重要的意义。不同的结构体成员顺序代表了不同的结构体类型。
结构体成员名字是以大写字母开头的,那么该成员就是导出的。
一个命名为S的结构体类型将不能再包含S类型的成员:因为一个聚合的值不能包含它自身。但是S类型的结构体可以包含*S指针类型的成员,这可以让我们创建递归的数据结构。
type tree struct {
value int
left, right *tree
}
结构体类型的零值是每个成员都是零值。
没有任何成员的结构体是空结构体,写作struct{}。它的大小为0,也不包含任何信息。在只强调存在性而不强调内容的场景下,空结构体很有价值,因为可以节约空间。
结构体字面值
结构体字面值可以指定每个成员的值。
//第一种写法
type Point struct{ X, Y int }
p := Point{1, 2}
//第二种写法:
p := Point{X:1, Y:2}
上面两种方法中,第一种方法适用于结构体小且变化不大的情况下,因为每个成员的类型和顺序被严格限制,一旦结构体定义有变化,编译就不能通过。 第二种方法更常用,就是以成员名字和相应的值来初始化,可以包含部分或全部的成员。
如果将结构体作为函数参数传递时,通常用指针。因为考虑效率和对原始变量的修改能够生效,传递指针很有必要。
func AwardAnnualRaise(e *Employee) {
e.Salary = e.Salary * 105 / 100
}
//创建并初始化一个结构体变量
pp := &Point{1, 2}
结构体比较
两个结构体可以使用==或!=运算符进行比较。相等比较运算符==将比较两个结构体的每个成员。
因为结构体可以进行判等操作,所以可以作为map的key。
type address struct {
hostname string
port int
}
hits := make(map[address]int)
hits[address{"golang.org", 443}]++
匿名结构体
var user struct{Name string; Age int}
user.Name = "郑凯"
user.Age = 18
fmt.Printf("%#v\n", user)
匿名成员
只声明一个成员对应的数据类型而不指名成员的名字,这类成员就叫匿名成员。 匿名成员可以是普通变量,也可以是结构体。这里我们只讨论结构体。 普通变量的匿名成员不推荐这么写。
通过结构体类型的匿名成员,我们就可以直接访问叶子属性而不需要给出完整的路径。其实他们不是真的没有名字,就是命名的类型名字,但是这些名字在点操作符中可以“省略”,达到了“匿名”的效果,简化了代码(语法糖)。
type Point struct {
X, Y int
}
type Circle struct {
//Center Point
Point
Radius int
}
type Wheel struct {
//Circle Circle
Circle
Spokes int
}
var w Wheel
//w.Circle.Center.X = 8
//w.Circle.Center.Y = 8
//w.Circle.Radius = 5
// equivalent to w.Circle.Point.X = 8
w.X = 8
// equivalent to w.Circle.Point.Y = 8
w.Y = 8
// equivalent to w.Circle.Radius = 5
w.Radius = 5
因为匿名成员也有一个隐式的名字,因此不能同时包含两个类型相同的匿名成员,这会导致名字冲突。
当一个结构体包含了另外一个结构体之后,不仅仅是获得了匿名成员类型的所有成员,而且也获得了该类型导出的全部的方法。这个机制可以用于将一些有简单行为的对象组合成有复杂行为的对象。从而变相的达到类似“继承”的效果,但又比继承丰富和强大以及灵活。
结构体与JSON序列化
- 序列化:把编程语言里面的数据转换成JSON格式的字符串
- 反序列化:把JONS格式的字符串转成编程语言中的对象
package main
import (
"fmt"
"encoding/json"
)
type Student struct {
Id int `json:"id"` //通过tag实现json序列化该字段时的key
Name string `json:"name"`
}
func main() {
stu1 := &Student{
Id: 1,
Name: "郑凯",
}
fmt.Println("序列化:把编程语言里面的数据转换成JSON格式的字符串")
v, err := json.Marshal(stu1)
if err != nil {
fmt.Println("JSON格式化出错!")
fmt.Println(err)
return
}
fmt.Println(v) //[]byte
fmt.Println(string(v)) //把[]byte转换成string
fmt.Printf("%#v\n", string(v)) //看真实的格式
fmt.Println("反序列化:把JONS格式的字符串转成编程语言中的对象")
str := string(v)
// str := "{\"Id\":1,\"Name\":\"郑凯\"}"
var stu2 = &Student{}
json.Unmarshal([]byte(str), stu2)
fmt.Println(*stu2)
fmt.Printf("%T\n", stu2)
}
输出结果如下:
结构体tag
tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来。例如上面例子中定义json序列化时的tag。
关于结构体Tag的详细解读,在 《凯冰科技移动应用开发手册》 的第三章第7小节 结构体Tag使用 有详细总结,大家可以移步去查看。
方法(Method)和接受者(Receiver)
Go语言中的方法Method是一种作用于特定类型变量的函数。这种特定类型变量叫做接受者(Receiver),类似于其它语言中的this或者self。但是要注意,以下两种情况:
- 不能给系统类型添加方法
- 不能给别的包定义的类型添加方法
相比于谁都可以调用的函数(Function),方法是某个具体的类型才能调用的函数。
func (接受者变量 接受者类型) 方法名(参数列表) (返回参数){
函数体
}
- 接受者变量:官方建议使用接受者类型名的第一个小写字母,比如Employee结构体可以用e
- 接受者类型:可以是指针类型和非指针类型,一般情况下都用指针类型。如果修改接受者中的值,必须用指针类型
type student struct {
name string
age int
gender string
hobby []string
}
//非指针类型接受者
func (s student) methodTest() {
//非指针类型是不能改值
s.age = 20
fmt.Printf("学生的姓名是%s\n", s.name)
}
//指针类型接受者
func (s *student) methodTestUpdate() {
s.age = 20
fmt.Printf("学生的姓名是%s\n", s.name)
}
func main() {
s := student{
name: "aaa",
age: 18,
}
s.methodTest() //学生的姓名是aaa
// 不能改值
fmt.Printf("学生的年龄是%d\n", s.age) //学生的年龄是18
s2 := &student{
name: "bbb",
age: 18,
}
s2.methodTestUpdate() //学生的姓名是bbb
fmt.Printf("学生的年龄是%d\n", s2.age) //学生的年龄是20
}
以上就是结构头Struct所有的知识点,你学会了吗?