Go part 5 结构体,方法与接收器
结构体
结构体定义
结构体的定义只是一种内存布局的描述(相当于是一个模板),只有当结构体实例化时,才会真正分配内存空间
结构体是一种复合的基本类型,通过关键字 type 定义为 自定义 类型后,使结构体更便于使用
定义一个简单的结构体:
- 类型名(Point)在同一个包内不能重复
- 字段名(X, Y)必须唯一
type Point struct { X int Y int }
同类型的变量也可以写在一行:
type Color struct { R, G, B byte }
实例
结构体实例与实例间的内存是完全独立的,
可以通过多种方式实例化结构体,根据实际需求选用不同的写法
1)基本的实例化形式(不推荐)
结构体是一种值类型,可以像整型,字符串一样,以 var 开头的方式声明结构体即可完成实例化
基本实例化格式:
var ins T 其中,T 为结构体类型,ins 为结构体的实例
var ins *T
创建指针类型的结构体,T 为结构体指针类型
Demo
func main(){
type Person struct{
Name string
Age int
}
var p Person
fmt.Printf("%T\n", p)
p.Name = "johny"
p.Age = 12
fmt.Println(p.Name, p.Age)
}
运行结果:
main.Person
johny 12
用声明的方式创建指针类型的结构体,然后进行赋值会触发 panic(空指针引用)
func main(){ type Person struct{ Name string Age int } var p1 *Person // var p1 *Person = new(Person) (*p1).Name = "anson" (*p1).Age = 13 fmt.Println((*p1).Name, (*p1).Age) } 运行结果: panic: runtime error: invalid memory address or nil pointer dereference
2)创建指针类型的结构体(推荐使用)
Go语言中,可以使用 new 关键字对值类型(包括结构体,整型,字符串等)进行实例化,得到指针类型
ins := new(T) 其中,T 为结构体类型,ins 为指针类型 *T
Demo(定义并实例化一个游戏玩家信息的结构体)
func main(){ type Player struct{ Name string HealthPoint int MagicPoint int } player := new(Player) fmt.Printf("%T\n", player) player.Name = "johny" player.HealthPoint = 100 player.MagicPoint = 100 fmt.Println(player.Name, player.HealthPoint, player.MagicPoint) } 运行结果: *main.Player johny 100 100
3)使用 键值对 初始化结构体(实例化时直接填充值)
每个键对应结构体中的一个字段,值对应字段中需要初始化的值
键值对的填充是可选的,不需要初始化的字段可以不在初始化语句块中体现(字段的默认值,是字段类型的默认值,例如:整数是0,字符串是 "",布尔是 false,指针是 nil 等)
实例化结构体
player := Player{ Name: "johny", HealthPoint: 100, MagicPoint: 100, } 其中,player是结构体实例,Player是结构体类型名,中间是键值对(字段名: 初始值)
实例化指针类型的结构体
player := &Player{ Name: "johny", HealthPoint: 100, MagicPoint: 100, }
结构体的嵌套(递归)
结构体成员中只能包含结构体的指针类型,包含非指针类型会引起编译错误(invalid recursive type 'People')
func main(){ type People struct{ Name string Child *People } relation := People{ Name: "grandPa", Child: &People{ Name: "father", Child: &People{ Name: "me", }, }, } // 与上面等效 me := People{ Name: "me", } father := People{ Name: "father", Child: &me, } grandPa := People{ Name: "grandPa", Child: &father, } fmt.Printf("%T\n%v\n", relation, relation.Child.Child.Name) fmt.Printf("%T\n%v\n", grandPa, grandPa.Child.Child.Name) } 运行结果: main.People me main.People me
匿名结构体
func main(){ ins := struct{ Name string Age int }{ Name: "johny", Age: 12, } fmt.Printf("%T\n", ins) fmt.Println(ins.Name, ins.Age) } 运行结果: struct { Name string; Age int } johny 12
模拟构造函数 初始化结构体
如果使用结构体来描述猫的特性,那么根据猫的名称与颜色可以有不同的类型,那么可以使用不同名称与颜色可以构造不同猫的实例:
type Cat struct{ Name string Color string } func NewCatByName(name string) *Cat { return &Cat{ Name: name, } } func NewCatByColor(color string) *Cat { return &Cat{ Color: color, } } func main(){ cat1 := NewCatByName("johny") cat2 := NewCatByColor("white") fmt.Printf("%T\n%T\n", cat1, cat2) fmt.Printf("%v\n%v\n", cat1.Name, cat2.Color) } 运行结果: *main.Cat *main.Cat johny white
带有 继承关系 的结构体的构造与初始化
猫是基本结构体(只有姓名和颜色)
黑猫继承自猫,是子结构体(不仅有姓名和颜色,还有技能)
使用不同的两个构造函数分别构造猫与黑猫两个结构体实例:
// 猫的结构体 type Cat struct{ Name string Color string } // 构造猫的函数 func NewCat(name, color string) *Cat { return &Cat{ Name: name, Color: color, } } // 黑猫的结构体(继承了猫,增加了技能字段) type BlackCat struct{ Cat Skill string } // 构造黑猫的函数(不能用) //func NewBlackCat(name, color, skill string) *BlackCat { // return &BlackCat{ // Name: name, // Color: color, // Skill: skill, // } //} // 构造黑猫的函数 func NewBlackCat(name, color, skill string) *BlackCat { blackCat := &BlackCat{} blackCat.Name = name blackCat.Color = color blackCat.Skill = skill return blackCat } func main(){ cat := NewCat("tom", "white") blackCat := NewBlackCat("blackTom", "black", "climb tree") fmt.Printf("%T\n%T\n", cat, blackCat) fmt.Printf("%v\n%v\n", cat.Name, cat.Color) fmt.Printf("%v\n%v\n%v\n", blackCat.Name, blackCat.Color, blackCat.Skill) } 运行结果: *main.Cat *main.BlackCat tom white blackTom black climb tree
Cat 结构体类似于面向对象中的“基类”。BlackCat 嵌入 Cat 结构体,类似于面向对象中的“派生”。实例化时,BlackCat 中的 Cat 也会一并被实例化
结构体匿名字段
上面的 黑猫 与 猫 的继承关系中,定义黑猫字段的时候,就用到了匿名字段:
// 黑猫的结构体(继承了猫,增加了技能字段) type BlackCat struct{ Cat Skill string }
1)匿名的结构体字段:
- 可以直接访问其成员变量:上述继承例子中的:blackCat.Name;也可以使用详细的字段一层层的进行访问(字段名就是它的类型名 Cat)
2)匿名的基本类型字段:
type Data struct { int float32 bool } func main(){ var data Data data.int = 100 fmt.Println(data.int, data.float32, data.bool) } 运行结果: 100 0 false
一个结构体中只能有一个同类型的匿名字段,不需要担心结构体字段重复问题
结构体字段标签
结构体标签是指对结构体字段的额外信息,进行 json 序列化及对象关系映射(Object Relational Mapping)时都会用到结构体标签,标签信息都是静态的,无须实例化结构体,可以通过反射拿到(反射后面会有记录)
Tag 在结构体字段后面书写,格式如下:
由一个或多个键值组成,键值对之间使用空格分隔
`key1:"value1" key2:"value2"`
demo:使用反射获取结构体的标签信息(先只要只要标签信息是有用的就行,反射知识点在后面学到)
package main import ( "fmt" "reflect" ) type Dog struct { Name string `json:"name" class_grade:"02"` } func main(){ var dog Dog = Dog{} typeOfDog := reflect.TypeOf(dog) dogFieldNmae, ok := typeOfDog.FieldByName("Name") if ok { fmt.Println(dogFieldNmae.Tag.Get("json"), dogFieldNmae.Tag.Get("class_grade")) } } 运行结果: name 02
结构体标签格式错误导致的问题
package main import ( "fmt" "reflect" ) func main() { type cat struct { Name string Type int `json: "type" id:"100"` } typeOfCat := reflect.TypeOf(cat{}) if catType, ok := typeOfCat.FieldByName("Type"); ok { fmt.Printf("'%v'", catType.Tag.Get("json")) } } 运行结果: '' //空字符串 在json:和"type"之间增加了一个空格。这种写法没有遵守结构体标签的规则,因此无法通过 Tag.Get 获取到正确的 json 对应的值
方法与接收器
方法(method)是一种作用于特定类型的函数,这种特定类型叫做接收器(receiver)(目标接收器)
如果将特定类型理解为结构体或“类”时,接收器的概念就相当于是实例,也就是其它语言中的 this 或 self
接收器的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法
为结构体添加方法
需求说明:使用背包作为“对象”,将物品放入背包的过程作为“方法”,通过面向过程的方式和结构体的方式来解释“方法”的概念
1)面向过程方式:
type Bag struct{ items []string } func put(bag *Bag, item string) { bag.items = append(bag.items, item) } func main(){ var bag *Bag = new(Bag) var item string = "foods" put(bag, item) fmt.Println(bag.items) } 运行结果: [foods]
2)结构体方法:
为 *Bag 创建一个方法,(bag *Bag) 表示接收器,即 put 方法作用的对象实例
type Bag struct{ items []string } func (b *Bag) put(item string) { b.items = append(b.items, item) } func main(){ var bag *Bag = new(Bag) var item string = "foods" bag.put(item) fmt.Println(bag.items) } 运行结果: [foods]
结构体方法的继承
模拟面向对象的设计思想(人和鸟的特性)
type Flying struct {} type Walkable struct {} func (f Flying) fly(){ fmt.Println("can fly") } func (w Walkable) walk(){ fmt.Println("can walk") } type Person struct { Walkable } type Bird struct { Flying Walkable } func main(){ var p *Person = new(Person) fmt.Printf("%T\n", p) p.walk() var b *Bird= new(Bird) fmt.Printf("%T\n", b) b.fly() b.walk() } 运行结果: *main.Person can walk *main.Bird can fly can walk
为任意类型添加方法
因为结构体也是一种类型,给其它类型添加方法和给结构体添加方法一样
给基本类型添加方法:
type myInt int func (a *myInt) set(num int){ *a = myInt(num) } func main(){ var a myInt a.set(66) fmt.Printf("%T %v\n", a, a) } 运行结果: 66
time 包中的基本类型方法:
time.Second 的类型是 Duration,而 Duration 实际是一个 int64 的类型
对于 Duration 类型有一个 String() 方法,可以将 Duration 的值转为字符串
func main(){
var a string = (time.Second*2).String()
var b time.Duration = time.Second*2
fmt.Printf("%T %v\n", a, a)
fmt.Printf("%T %v\n", b, b)
}
运行结果:
string 2s
time.Duration 2s
接收器
每个方法只能有一个接收器,如下图:
接收器的格式如下:
func (接收器变量 接收器类型) 方法名(参数列表) (返回参数) { 函数体 }
各部分说明:
- 接收器变量:命名,官方建议使用接收器类型的第一个小写字母,例如,Socket 类型的接收器变量应该为 s,Connector 类型应该命名为 c
- 接收器类型:与参数类似,可以是指针类型和非指针类型,两种接收器会被用于不同性能和功能要求的代码中(需要做更新操作时,用指针类型)
- 方法名、参数列表、返回参数:与函数定义相同
1)理解指针类型的接收器
更接近于面向对象中的 this 或 self;由于指针的特性,调用方法时,修改接收器指针的任意成员变量,在方法结束后,修改都是有效的
Demo:接收一个结构体的指针,并做修改
// 定义属性结构 type Person struct{ name string } // 设置name func (p *Person) setName (name string){ p.name = name } // 获取name func (p *Person) getName()(name string){ return p.name } func main(){ var person *Person = new(Person) person.setName("johny") name := person.getName() fmt.Println(name) }
2)理解非指针类型的接收器
当方法作用于非指针接收器时,会在代码运行时将接收器的值复制一份;可以获取接收器的成员值,但修改后无效
Demo:定义一个空间坐标(二维)的结构体,接收非指针的结构体,两点进行相加
type Point struct{ x int y int } func (p1 Point) add(p2 Point) Point{ return Point{p1.x + p2.x, p1.y + p2.y} } func main(){ var p1 Point = Point{1,1} var p2 Point = Point{1,2} p := p1.add(p2) fmt.Printf("(%d, %d)\n", p.x, p.y) } 运行结果: (2, 3)
3)关于指针和非指针接收器的使用
在计算机中,小对象由于值复制时的速度较快,所以适合使用非指针接收器。大对象因为复制性能较低,适合使用指针接收器(在接收器和参数间传递时不进行复制,只是传递指针)
实例:二维矢量模拟玩家移动
在游戏中,一般使用二维矢量保存玩家的位置,使用矢量计算可以计算出玩家移动的位置,下面的 demo 中,首先实现二维矢量对象,接着构造玩家对象,最后使用矢量对象和玩家对象共同模拟玩家移动的过程
1)实现二维矢量结构
矢量是数据中的概念,二维矢量拥有两个方向的信息,同时可以进行加、减、乘(缩放)、距离、单位化等计算
在计算机中,使用拥有 x 和 y 两个分量的 Vecor2 结构体实现数学中二维向量的概念,如下:
package main import "math" type Vector struct { x float32 y float32 } // 坐标点操作的方法 func (v1 Vector) add(v2 Vector) Vector { return Vector{v1.x + v2.x, v1.y + v2.y} } func (v1 Vector) sub(v2 Vector) Vector { return Vector{v1.x - v2.x, v1.y - v2.y} } func (v1 Vector) multi(speed float32) Vector { return Vector{v1.x * speed, v1.y * speed} } // 计算距离 func (v1 Vector) distanceTo(v2 Vector) float32 { dx := v1.x - v2.x dy := v1.y - v2.y distance := math.Sqrt(float64(dx*dx + dy*dy)) return float32(distance) } // 矢量单位化 func (v1 Vector) normalize() Vector { mag := v1.x * v1.x + v1.y * v1.y if mag > 0 { oneOverMag := 1 / float32(math.Sqrt(float64(mag))) return Vector{v1.x * oneOverMag, v1.y * oneOverMag} } else { return Vector{0, 0} } }
2)实现玩家对象
玩家对象负责存储玩家的当前位置、目标位置和移动速度,使用 moveTo() 为玩家设定目的地坐标,使用 update() 更新玩家坐标
package main type Player struct { currentVector Vector targetVector Vector speed float32 } // 初始化玩家,设置速度 func newPlayer(speed float32) Player { return Player{speed: speed} } // 设置目标位置 func (p *Player) moveTo(v Vector) { p.targetVector = v } // 获取当前位置 func (p Player) posision() Vector { return p.currentVector } // 是否到达目标位置 func (p Player) isArrived() bool { return p.currentVector.distanceTo(p.targetVector) < p.speed } // 更新玩家位置 func (p *Player) update() { // 使用矢量减法,将目标位置 targetVector 减去当前位置 currentVector,即可得出移动方向的新矢量 directionVector := p.targetVector.sub(p.currentVector) // 矢量单位化 normalizeVector := directionVector.normalize() // 计算 x, y 方向上改变的距离 pointChange := normalizeVector.multi(p.speed) // 玩家新的坐标位置 newVector := p.currentVector.add(pointChange) // 更新玩家坐标 p.currentVector = newVector }
更新坐标稍微复杂一些,需要通过矢量计算获得玩家移动后的新位置,步骤如下:
- 使用矢量减法,将目标位置(targetPos)减去当前位置(currPos)即可计算出位于两个位置之间的新矢量
- 使用 normalize() 方法将方向矢量变为模为 1 的单位化矢量,
- 然后用单位化矢量乘以玩家的速度,就能得到玩家每次分别在 x, y 方向上移动的长度
- 将目标当前位置的坐标与移动的坐标相加,得到新位置的坐标,并做修改
3)主程序
玩家移动是一个不断更新位置的循环过程,每次检测玩家是否靠近目标点附近,如果还没有到达,则不断地更新位置,并打印出玩家的当前位置,直到玩家到达终点
package main import "fmt" func main(){ // 创建玩家,设置玩家速度 var p Player = newPlayer(0.2) fmt.Println(p.speed) // 设置玩家目标位置 p.moveTo(Vector{2, 2}) p.currentVector = Vector{1, 3} fmt.Println(p.targetVector) for !p.isArrived() { // 更新玩家坐标位置 p.update() // 打印玩家位置 fmt.Println(p.posision()) // 一秒更新一次 time.Sleep(time.Second) } fmt.Println("reach destination~") }
- 将 Player 实例化,设定玩家终点坐标,当前坐标
- 更新玩家位置
- 每次移动后,打印玩家的位置坐标
- 延时 1 秒(便于观察效果)
实例(python版本)
抽空写了个python版本,加强理解
# coding=utf-8 import math import time # 坐标类 class Vector(object): def __init__(self, x=0, y=0): self.x = x self.y = y # 相加 def add(self, vector): self.x += vector.x self.y += vector.y # 相减 def sub(self, vector): x = self.x - vector.x y = self.y - vector.y return Vector(x, y) # 相乘 def multi(self, speed): self.x *= speed self.y *= speed return self # 计算距离 def distance(self, vector): dx = self.x - vector.x dy = self.y - vector.y return math.sqrt(dx ** 2 + dy ** 2) # 矢量单位化 def normalize(self): mag = self.x ** 2 + self.y ** 2 if mag > 0: one_over_mag = 1 / math.sqrt(mag) vector = Vector(x=self.x * one_over_mag, y=self.y * one_over_mag) else: vector = Vector() return vector # 玩家类 class Player(object): def __init__(self, current_vector=None, target_vector=None, speed=0): self.current_vector = current_vector self.target_vector = target_vector self.speed = speed # 获取玩家坐标 def get_current_vector(self): return self.current_vector # 判断是否到达终点 def is_arrived(self): return self.current_vector.distance(self.target_vector) < self.speed # 更新玩家位置 def update_vector(self): # 获取方向矢量(固定值) direction_vector = self.target_vector.sub(self.current_vector) # 矢量单位化(固定值) normalize_vector = direction_vector.normalize() # 根据速度计算 x, y 方向上前进的长度 ongoing_vector = normalize_vector.multi(self.speed) # 更新位置 self.current_vector.add(ongoing_vector) if __name__ == '__main__': p = Player() p.current_vector = Vector(0, 0) p.target_vector = Vector(2, 2) p.speed = 0.2 while not p.is_arrived(): p.update_vector() print(f"({p.current_vector.x}, {p.current_vector.y})") time.sleep(1) print("arrive at the destination")