结构体(Struct)
Go语言通过自定义方式形成新的类型,而结构体是类型中带有成员的复合类型。Go语言使用结构体和结构成员描述真实世界的实体和实体对应的各种属性。
- 类型可以被实例化,使用new或&,构造的实例的类型是类型的指针。
- 结构体成员是由一系列成员变量构成,也成为‘字段’
-
字段特性
- 拥有自己的类型和值
- 字段名必须是唯一的
- 字段类型可以也是结构体,甚至是字段所在结构体的类型
- Go语言中不支持“类”的概念,但结构体与类都是复合结构体,但Go语言中结构体的内嵌配合接口比面向对象具有更高的扩展性和灵活性。
- Go中不仅结构体可以拥有方法,每种自定义的类型也可以拥有方法
1.定义结构体
type structName struct {
name1 name1Type
name2 name2Type
}
- 类型名,标识结构体的名称,同一个包中不能重复
- struct{} 表示结构体的类型。
- 结构体表示一个点结构,包含X, Y两个整型分量
type Point struct {
X int
Y int
}
- 同类型的变量也可以写在一行
type Color struct {
R, G, B byte
}
2. 实例化结构体-为结构体分配内存并初始化
- 结构体的定义是一种内存分布的描述,只有实例化时才会真正的分配内存。也就是,实例化后才能使用。
- 实例化:根据结构体定义的格式创建一份与格式一致的内存区域,结构体实例间的内存是完全独立的。
2.1 基本的实例化形式
- 使用 var 方法实现实例化
// 基本实例化格式
var ins T
// 实例化Point结构
type Point struct {
X int
Y int
}
var p Point
p.X = 10 // 使用“.”来访问结构体的成员变量,赋值方法与普通的一致
p.Y = 20
2.2 创建指针类型的结构体
- 使用new关键字进行实例化,变成指针类型的结构体
// 基本格式
ins := new(T)
// 实例化玩家结构体
type Player struct {
Name string
HealthPoint int
MagicPoint int
}
tank := new(Player)
tank.Name = "Canon"
tank.HealthPoint = 300
Go 语言中为了方便开发者访问结构体指针的成员变量,使用了语法糖(Syntactic sugar)技术,将ins.Name 变为(*ins).Name
2.3 去结构体的地址实例化
- 对结构体进行“&"取地址操作时,视为对该类型进行一次new的实例化操作
// 基本格式
ins := &T{}
// 对Command进行指针地址的实例化
type Command struct {
Name string // 指令名称
Var *int // 指令绑定的变量
Comment string //指令的注释
}
var version int = 1
cmd := &Command{}
cmd.Name = "version"
cmd.Var = &version
cmd.Comment = "show version"
- 取地址实例化时最广泛的结构体实例化方式
func newCommand(name string, varref *int, cooment string) *Command {
return &Command {
Name: name,
Var: varref,
Comment: comment,
}
}
cmd = newCommand(
"version",
&version,
"show version",
)
3. 初始化结构体的成员变量
- 结构体在实例化时可以直接对成员变量进行初始化。初始化有两种形式:一种是字段"key:value"形式以及多个值的列表形式
- 键值对形式适合多字段结构体,字段少的用列表形式
3.1 使用"键值对"初始化结构体
// 基本格式
ins := 结构体类型名 {
字段1: 字段1的值
字段2: 字段2的值
...
}
// 描述人物关联
type People struct {
name string
child *People
}
relation := &People {
name: "爷爷",
child: &People{
name: "爸爸",
child: &People {
name: "我",
},
},
}
3.2 使用多个值的列表初始化结构体
// 基本格式
ins := 结构体类型名{
value1,
value2,
...
}
// 地址结构
type Address struct {
Province string
City string
ZipCode int
PhoneNumber string
}
addr := Address {
"四川",
"成都",
610000,
"0",
}
fmt.Println(addr)
3.3 初始化匿名结构体
ins := struct {
//匿名结构体字段定义
字段1 字段1类型
字段2 字段2类型
} {
// 字段值初始化
name1: value1,
name2: value2,
} //也可以不初始化
// 示例消息结构
func printMsgType(msg *struct{
id int
data string
}) {
fmt.Printf("%T\n", msg) // 将msg的类型名打印出来
}
func main() {
msg := &struct {
id int
data string
}{
1024,
"hello",
}
printMsgType(msg)
}
// 输出:*struct {id int; data string}
// 匿名结构体的类型名是结构体包含字段成员的详细描述,匿名结构体在使用时需要重新定义,造成大量重复的代码。因此比较少使用
4. 构造函数-结构体和类型的一系列初始化操作的函数封装
- Go语言的类型或结构体没有构造函数的功能,结构体的初始化过程可以使用函数封装实现
4.1 多种方式创建和初始化结构体--模拟构造函数重载
type Cat struct {
Color string
Name string
}
func NewCatByName(name string) *Cat {
return &Cat{
Name: name,
}
}
func NewCatByColor(color string) *Cat{
return &Cat{
Color: color
}
}
4.2 带有父子关系的结构体的构造和初始化--模拟父级构造调用
type BlackCat struct{
Cat
}
// 构造基类
func NewCat(name string) *Cat{
return &Cat {
Name: name,
}
}
// 构造子类
func NewBlackCat(color string) *BlackCat {
cat := &BlackCat{}
cat.color = color
return cat
}
5. 方法
- 方法是一种作用于特定类型变量的函数,这种特定类型的变量叫做接收器(receiver)。
5.1 为结构体添加方法
- 面向过程实现方法
type Bag struct {
items []int
}
// 将一个物品放入背包中
func Insert(b *Bag, itemid int) {
b.items = append(b.items, itemid)
}
func main() {
bag := new(Bag)
Insert(bag, 1001)
}
- Go语言的结构体方法
type Bag struct {
items []int
}
func (b *Bag) Insert(itemid int) {
b.itmes = append(b.items, itemid)
}
func main() {
b.Insert(1001)
}
5.2 接收器--方法作用的目标
func (接收器变量 接收器类型) 方法名(参数列表) (返回参数) {
函数体
}
- 理解指针类型的接收器
- 指针类型的接收器由一个结构体的指针组成,更接近于面向对象中的this或者self
- 由于指针的特性,调用方法时,修改接收器指针的任意成员变量,方法结束后,修改都是有效的。
type Property struct {
value int //属性值
}
func (p *Property) SetValue(v int) {
p.value = v
}
func (p *Property) Value() int {
return p.value
}
func main() {
p := new(property)
p.SetValue(100)
fmt.Println(p.Value())
}
- 理解非指针类型的接收器
- 当方法作用于非指针接收器时,Go语言在代码运行时将接受器的值复制一份。非指针接收器获取接收器的成员值,但修改后无效。
type Point struct {
X int
Y int
}
// 非指针接收器的加方法
func (p Point) Add(other Point) Point {
return Point{p.X + other.X, p.Y + other.Y}
}
func main() {
p1 := Point{1, 1}
p2 := Point{2, 2}
result := p1.Add(p2)
fmt.Println(result)
}
5.3 二维矢量模拟玩家移动
- 实现二维矢量的结构
type Vec2 struct {
X, Y float32
}
func (v Vec2) Add(other Vec2) Vec2 {
return vec2{
v.X + other.X,
v.Y + other.Y,
}
}
func (v Vec2) Sub(other Vec2) {
return Vec2 {
v.X - other.X,
v.Y - other.Y,
}
}
func (v Vec2) Scale(s float32) Vec2 {
return Vec2{v.X * s, v.Y * s}
}
func (v Vec2) DistanceTo(other Vec2) float32 {
dx := v.X - other.X
dy := v.Y - other.Y
return float32(math.Sqrt(float64(dx*dx + dy*dy)))
}
func (v Vec2) Normalize() Vec2 {
mag := v.X * v.X + v.Y * v.Y
if mag > 0 {
oneOverMag := 1 / float32(math.Sqrt(float64(mag)))
return Vec2 {v.X * oneOverMag, v.Y * oneOverMag}
}
return Vec2{0, 0}
}
type Player struct {
currPos Vec2
targetPos Vec2
speed float32
}
func (p *Player) MoveTo(v Vec2) {
p.targetPos = v
}
func (p *Player) Pos() Vec2 {
return p.currPos
}
func (p *Player) IsArrived() bool {
return p.currPos.DistanceTo(p.targetPos) < p.speed
}
func (p *Player) Update() {
if !p.IsArrived() {
dir := p.targetPos.Sub(p.currPos).Normalize()
newPos := p.currPos.Add(dir.Scale(p.speed))
p.currPos = newPos
}
}
func NewPlayer(speed float32) *Player {
return &Player {
speed: speed,
}
}
- 处理移动逻辑
p := NewPlayer(0.5)
p.MoveTo(Vec2{3, 1})
for !p.IsArrived() {
p.Update()
fmt.Println(p.Pos())
}
5.4 为类型添加方法
- Go语言可以给任何方法添加方法
- 为基本类型添加方法
type MyInt int
func (m MyInt) IsZero() bool {
return m == 0
}
func (m MyInt) Add(other int) int {
return other+ int(m)
}
func main() {
var b MyInt
fmt.Println(b.IsZero())
b = 1
fmt.Println(b.Add(2))
}
- http包中的类型方法
- http包中的NewRequest()方法可以创建一个HTTP请求,填充请求中的http头(req.Header), 再调用http.Client的Do包方法,将传入的HTTP请求发送出去
func main() {
client := &http.Client{}
req, err := http.NewRequest("POST", "http://www.163.com/",
strings.NewReader("key=value"))
if err != nil {
fmt.Println(err)
os.Exit(1)
return
}
req.Header.Add("User-Agent", "myClient")
resp, err := client.Do(req)
if err != nil {
fmt.Println(err)
os.Exit(1)
return
}
data, err := ioutil.ReadAll(resp.Body)
fmt.Println(string(data))
defer resp.Body.Close()
}
- http.Header的部分定义如下
type Header map[string][]string // Header是一个以字符串为键,字符串切片为值的映射
func (h Header) Add(key, value string) { // map为引用类型,因此(h Header)的非指针接收器,也可以修改map的值
textproto.MIMEHeader(h).Add(key, value)
}
func (h Header) Set(key, value string) {
textproto.MIMEHeader(h).Set(key, value)
}
func (h Header) Get (key string) string {
return textproto.MIMEHeader(h).Get(key)
}
- time包中的类型方法
- time包主要用于时间的获取和计算
func main() {
fmt.Println(time.Second.String())
}
// time.Second的定义
const (
Nanosecond Duration = 1
Microsecond = 1000 * Nanosecond
Millisecond = 1000 * Microsecond
Second = 1000 * Millisecond
Minute = 60 * Second
Hour = 60 * Minute
)
// Second的类型为Duration,而Duartion实际是一个int64的类型,定义如下:
type Duration int64
func (d Duration) String() string {
...
return string(buf[w:])
}
Duration.String可以将Duration的值转为字符串
5.5 示例:使用事件系统实现事件的响应和处理
- GO语言将类型的方法和普通函数视为一个概念,简化回调类型的复杂性。
-
方法和函数的统一调用
- 让一个结构体的方法的参数和一个普通函数的参数完全一样,也就是方法和函数的签名一致。然后使用与签名一致的变量分别赋值方法与函数。
type class struct {
}
func (c *class) Do(v int) {
fmt.Println("call method do: ", v)
}
func funcDo(v int) {
fmt.Println("call function do: ", v)
}
func main() {
var delegate func(int)
c := new(class)
delegate = c.Do
delegate(100)
delegate = funcDo
delegate(100)
}
- 无论是普通函数还是结构体方法,只要签名一致,与签名一致的函数变量就可以保存普通函数或结构体方法
-
事件系统基本原理
- 事件系统可以将事件派发者与事件处理者解耦。
-
事件系统的特性:
- 能实现事件的一方,根据事件的ID或名字注册对应的事件
- 事件发起者,会根据注册信息通知这些注册者
- 一个事件可以有多个实现方响应
-
事件注册
- 事件系统需要为外部提供一个注册入口,注册入口传入的事件名称和对应事件名称的响应函数,事件注册的过程就是将事件名称和响应函数关联并保存起来
var eventByName = make(map[string][]func(interface{})) func RegisterEvent(name string, callback func(interface{})) { list := eventByName[name] list = append(list, callback) eventByName[name] = list }
-
事件调用
+ 事件调用方是事发现场,负责将事件和事件发生的参数通过事件系统派发出去。事件注册时通过事件系统注册应该响应哪些事件及如何使用回调函数处理这些事情。
func CallEvent(name string, param interface{}) {
list := eventByName[name]
for _, callback := range list {
callback(param)
}
}
-
使用事件系统
type Actor struct { } func (a *Actor) OnEvent(param interface{}) { fmt.Println("actor event: ", param) } func GlobalEvent(param interface{}) { fmt.Println("global event:", param) } func main() { a := new(Actor) RegisterEvent("OnSkill", a.OnEvent) RegisterEvent("OnSkill", GlobalEvent) CallEvent("OnSkill", 100) } // 输出 acotr event: 100 global event: 100
- 全局事件和角色事件会按注册顺序顺序地触发
- 事件系统一般不保证同一个事件实现多个函数列表中的调用顺序,所有实现函数都是平等的,一个完善的事件系统还会提供移除单个和所有事件的方法。
6. 类型内嵌和结构体内嵌
- 结构体允许成员字段在声明时没有字段名而只有类型,这种形式的字段叫类型内嵌或匿名字段类型
type Data struct {
int
float32
bool
}
ins := &Data{
int: 10,
float32: 3.14,
bool: true,
}
-
声明结构体内嵌
type BasicColor struct { R, G, B float32 } type Color struct { Basic BasicColor Alpha float32 } func main() { var c Color c.Basic.R = 1 c.Basic.G = 1 c.Basic.B = 0 c.Alpha = 1 fmt.Printf("%+v", c) } // 结构体内嵌写法 type Color struct { BasicColor Alpha float32 } func main() { var c Color c.R = 1 c.G = 1 c.B = 0 c.Alpha = 1 fmt.Printf("%+v", c) }
-
结构体内嵌的特性
-
内嵌的结构体可以直接访问其成员变量
- 只用给出字段名即可
-
内嵌结构体的字段名时它的类型名
- 一个结构体只能嵌入一个同类型的成员
-
-
使用组合思想描述对象特性
- 面向对象中最好不要使用多重继承
- Go语言结构体就是一种组合特性,可以快速构建对象的不同特性
type Flying struct {} func (f *Flying) Fly() { fmt.Println("can fly") } type Walkable struct {} func (f *Walkable) Walk() { fmt.Println("can walk") } type Human struct { Walkable } type Bird struct { Walkable Flying } func main() { b := new(Bird) fmt.Println("Bird: ") b.Fly() b.Walk() h := new(Human) fmt.Println("Human: ") h.Walk() }
-
初始化结构体内嵌
- 将结构体内嵌的类类型作为字段名像普通结构体一样初始化
// 车轮 type Wheell struct { Size int } // 引擎 type Engine struct { Power int Type string } // 车 type Car struct { Wheel Engine } func main() { c := Car{ Wheel: Wheel{ Size: 18, }, Engine: Engine{ Type: "1.4T" Power: 143, }, } fmt.Printf("%+v\n", c) }
6.5 初始化内嵌匿名结构体
type Wheel struct {
Size int
}
type Car struct {
Wheel
Engine struct {
Power int
Type string
}
}
func main() {
c := Car{
Wheel: Wheel{
Size: 18,
},
Engine: struct {
Power int
Type string
}{
Type: "1.4T",
Power: 143,
},
}
fmt.Printf("%+v\n", c)
}
6.6 成员名字冲突
type A struct {
a int
}
type B struct {
a int
}
type C struct {
A
B
}
func main() {
c := &C{}
c.a = 2 // 编译报错,不知道是A/B的字段a
}
6.7 使用匿名结构体分离JSON数据
package main
import (
"encoding/json"
"fmt"
)
// 定义数据结构
type Screen struct {
Size float32
ResX, ResY int
}
type Battery struct {
Capacity int
}
// 准备Json数据
func genJsonData() []byte {
raw := &struct {
Screen
Battery
HasTouchID bool
}{
Screen: Screen{
Size: 5.5,
ResX: 1920,
ResY: 1080,
},
Battery: Battery{
2910,
},
HasTouchID: true,
}
//将数据序列化
jsonData, _ := json.Marshal(raw)
return jsonData
}
// 分离JSON数据
func main() {
// 通过json.Unmarshal反序列化JSON数据
jsonData := genJsonData()
fmt.Println(string(jsonData))
screenAndTouch := struct {
Screen
HasTouchID bool
}{}
// 反序列化到ScreenAndTouch中
json.Unmarshal(jsonData, &screenAndTouch)
fmt.Printf("%+v\n", screenAndTouch)
batteryAndTouch := struct {
Battery
HasTouchID bool
}{}
// 反序列化到batteryAndTouch
json.Unmarshal(jsonData, &batteryAndTouch)
fmt.Printf("%+v\n", batteryAndTouch)
}
有疑问加站长微信联系(非本文作者)