• 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
    ", 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
    ", 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
    %v
    ", relation, relation.Child.Child.Name)
        fmt.Printf("%T
    %v
    ", 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
    ", 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
    %T
    ", cat1, cat2)
        fmt.Printf("%v
    %v
    ", 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
    %T
    ", cat, blackCat)
        fmt.Printf("%v
    %v
    ", cat.Name, cat.Color)
        fmt.Printf("%v
    %v
    %v
    ", 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
    ", p)
        p.walk()
    
        var b *Bird= new(Bird)
        fmt.Printf("%T
    ", 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
    ", 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
    ", a, a)
        fmt.Printf("%T %v
    ", 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)
    ", 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
    每天都要遇到更好的自己.
  • 相关阅读:
    C++中static修饰的静态成员函数、静态数据成员
    C++友元函数、友元类
    C++异常处理
    运行时类型识别RTTI
    AD转换
    敏捷模式下的测试用例该如何存在?
    使用Postman轻松实现接口数据关联
    接口测试Mock利器-moco runner
    python测开平台使用dockerfile构建镜像
    MySQL – 用SHOW STATUS 查看MySQL服务器状态
  • 原文地址:https://www.cnblogs.com/kaichenkai/p/10953745.html
Copyright © 2020-2023  润新知