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
View Code 

 

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 对应的值
View Code

 

 

方法与接收器

方法(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}
    }
}
Vector

 

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
}
player

更新坐标稍微复杂一些,需要通过矢量计算获得玩家移动后的新位置,步骤如下:

  1. 使用矢量减法,将目标位置(targetPos)减去当前位置(currPos)即可计算出位于两个位置之间的新矢量
  2. 使用 normalize() 方法将方向矢量变为模为 1 的单位化矢量
  3. 然后用单位化矢量乘以玩家的速度,就能得到玩家每次分别在 x, y 方向上移动的长度
  4. 将目标当前位置的坐标与移动的坐标相加,得到新位置的坐标,并做修改

 

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~")
}
View Code
  1. 将 Player 实例化,设定玩家终点坐标,当前坐标
  2. 更新玩家位置
  3. 每次移动后,打印玩家的位置坐标
  4. 延时 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")
View Code

 

posted @ 2019-05-31 10:14  kaichenkai  阅读(470)  评论(0编辑  收藏  举报