结构体

结构体定义

结构体的定义只是一种内存布局的描述(相当于是一个模板),只有当结构体实例化时,才会真正分配内存空间

结构体是一种复合的基本类型,通过关键字 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}
}
}

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~")
}
  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")

Go part 5 结构体,方法与接收器的更多相关文章

  1. go结构体方法

    Golang中的方法是作用在特定类型的变量上,因此自定义类型,都可以有方法,而不仅仅是struct. 定义格式 func (var *Struct_Name) FuncName( var0, var1 ...

  2. Go 结构体方法

    #### Go 结构体方法本来今天有些事情忙的不准备更新内容了,后来提前完成了, 所以还是要更新了; 毕竟坚持本就是一件不容易的事情!加油,相信不管是大家还是我,都有一些事情想要做,那就坚持吧,剩下的 ...

  3. Go语言 - 结构体 | 方法

    自定义类型和类型别名 自定义类型 在Go语言中有一些基本的数据类型,如string.整型.浮点型.布尔等数据类型, Go语言中可以使用type关键字来定义自定义类型. 自定义类型是定义了一个全新的类型 ...

  4. go语言从例子开始之Example18_1.结构体中定义方法

    Go 支持在结构体类型中定义方法 . Example: package main import "fmt" type product struct{ name string num ...

  5. Go基础3:函数、结构体、方法、接口

    目录 1. 函数 1.1 函数返回值 同一种类型返回值 带变量名的返回值 函数中的参数传递 函数变量 1.2 匿名函数--没有函数名字的函数 在定义时调用匿名函数 将匿名函数赋值给变量 匿名函数用作回 ...

  6. 【2016-08-18】转载:总结C++中几种结构体初始化的方法

    作者:Ac_Von 博客地址:http://www.cnblogs.com/vongang/ 文章地址:http://www.cnblogs.com/vongang/archive/2011/07/3 ...

  7. 六、golang中的结构体和方法、接口

    结构体: 1.用来自定义复杂数据结构 2.struct里面可以包含多个字段(属性) 3.struct类型可以定义方法,注意和函数的区分 4.strucr类型是值类型 5.struct类型可以嵌套 6. ...

  8. 【转】golang 结构体和方法

    原文:https://www.jianshu.com/p/b6ae3f85c683 ---------------------------------------------------------- ...

  9. Go 语言 结构体和方法

    @ 目录 1. 结构体别名定义 2. 工厂模式 3. Tag 原信息 4. 匿名字段 5. 方法 1. 结构体别名定义 变量别名定义 package main import "fmt&quo ...

随机推荐

  1. 自定义控件之canvas变换和裁剪

    1.平移 //构造两个画笔,一个红色,一个绿色 Paint paint_green = generatePaint(Color.GREEN, Paint.Style.STROKE, 3); Paint ...

  2. java项目中ehcache缓存最简单用法

      java项目中ehcache缓存最简单用法: 1.下载ehcache-core-2.4.3.jar复制到项目的lib目录下 2.新建ehcache.xml文件,放置在项目src目录下的resour ...

  3. Qt编写自定义控件43-自绘电池

    一.前言 到了9102年了,现在智能手机不要太流行,满大街都是,甚至连爷爷奶奶级别的人都会用智能手机,本次要写的控件就是智能手机中的电池电量表示控件,采用纯painter绘制,其实也可以采用贴图,我估 ...

  4. Qt编写自定义控件41-自定义环形图

    一.前言 自定义环形图控件类似于自定义饼状图控件,也是提供一个饼图区域展示占比,其实核心都是根据自动计算到的百分比绘制饼图区域.当前环形图控件模仿的是echart中的环形图控件,提供双层环形图,有一层 ...

  5. React中如何实现模态框每次打开都是初始界面

    问题描述如下 解决方案:每次点击打开模态框的时候为,当前模态框设置一个独立的key值,代码如下: /* * 上传文件的模块框控制 * */ showFileModal = () => { thi ...

  6. MySQL数据同步交换

    一.为了解决数据同步汇聚,数据分发,数据转换,数据维护等需求,TreeSoft将复杂的网状的同步链路变成了星型数据链路.     TreeSoft作为中间传输载体负责连接各种数据源,为各种异构数据库之 ...

  7. 【计算机视觉】OpenCV读取视频获取时间戳等信息(PS:经测试并不是时间戳,与FFMPEG时间戳不一样)

    OpenCV中通过VideoCaptrue类对视频进行读取操作以及调用摄像头,下面是该类的API. 1.VideoCapture类的构造函数: C++: VideoCapture::VideoCapt ...

  8. 基于MSP430G2231实现的频率计

    基于MSP430G2231实现的频率计 声明:引用请注明出处http://blog.csdn.net/lg1259156776/ 系列博客说明:此系列博客属于作者在大三大四阶段所储备的关于电子电路设计 ...

  9. Opencv官方例程简介

    opencv sample文件夹例程 No1. adaptiveskindetector.cpp 利用HSV空间的色调信息的皮肤检测,背景不能有太多与肤色相似的颜色.效果不是特别好. No2. bag ...

  10. google搜索设置,在新的窗口打开