1. 结构体

Go语言可以通过自定义的方式形成新的类型,结构体就是这些类型中的一种复合类型,结构体是由零个或多个任意类型的值聚合成的实体,每个值都可以称为结构体的成员。

结构体成员也可以称为“字段”,这些字段有以下特性:

  • 字段拥有自己的类型和值;
  • 字段名必须唯一;
  • 字段的类型也可以是结构体,甚至是字段所在结构体的类型。

使用关键字 type 可以将各种基本类型定义为自定义类型,基本类型包括整型、字符串、布尔等。结构体是一种复合的基本类型,通过 type 定义为自定义类型后,使结构体更便于使用。

结构体的定义格式如下:

type 类型名 struct {
    字段1 字段1类型
    字段2 字段2类型
    …
}
1
2
3
4
5
  • 类型名:标识自定义结构体的名称,在同一个包内不能重复。
  • struct{}:表示结构体类型,type 类型名 struct{}可以理解为将 struct{} 结构体定义为类型名的类型。
  • 字段1、字段2……:表示结构体字段名,结构体中的字段名必须唯一。
  • 字段1类型、字段2类型……:表示结构体各个字段的类型。

示例:

type Point struct {
    X int
    Y int
}
1
2
3
4

颜色的红、绿、蓝 3 个分量可以使用 byte 类型:

type Color struct {
    R, G, B byte
}
1
2
3

结构体的定义只是一种内存布局的描述,只有当结构体实例化时,才会真正地分配内存

1.1 实例化

实例化就是根据结构体定义的格式创建一份与格式一致的内存区域,结构体实例与实例间的内存是完全独立的。

基本的实例化形式:

结构体本身是一种类型,可以像整型、字符串等类型一样,以 var 的方式声明结构体即可完成实例化。

var ins T
1

T 为结构体类型,ins 为结构体的实例。

package main

import "fmt"

type Point struct {
	X int
	Y int
}
func main() {
    //使用.来访问结构体的成员变量,结构体成员变量的赋值方法与普通变量一致。
	var p Point
	p.X = 1
	p.Y = 2
	fmt.Printf("%v,x=%d,y=%d",p,p.X,p.Y )
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

type Point struct {
	X int
	Y int
}
func main() {

	var p Point
	//p.X = 1
	//p.Y = 2
    //如果不赋值 结构体中的变量会使用零值初始化
	fmt.Printf("%v,x=%d,y=%d",p,p.X,p.Y )
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import "fmt"

type Point struct {
	X int
	Y int
}
func main() {
	//可以使用
	var p = Point{
		X: 1,
		Y: 2,
	}
    var p = Point{
		1,
		2,
	}
	fmt.Printf("%v,x=%d,y=%d",p,p.X,p.Y )
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

创建指针类型的结构体:

Go语言中,还可以使用 new 关键字对类型(包括结构体、整型、浮点数、字符串等)进行实例化,结构体在实例化后会形成指针类型的结构体。

ins := new(T)
1
  • T 为类型,可以是结构体、整型、字符串等。
  • ins:T 类型被实例化后保存到 ins 变量中,ins 的类型为 *T,属于指针。

下面的例子定义了一个玩家(Player)的结构,玩家拥有名字、生命值和魔法值:

type Player struct{
    Name string
    HealthPoint int
    MagicPoint int
}
tank := new(Player)
tank.Name = "码神"
tank.HealthPoint = 300
1
2
3
4
5
6
7
8

new 实例化的结构体实例在成员赋值上与基本实例化的写法一致。

取结构体的地址实例化:

在Go语言中,对结构体进行&取地址操作时,视为对该类型进行一次 new 的实例化操作,取地址格式如下:

ins := &T{}
1

其中:

  • T 表示结构体类型。
  • ins 为结构体的实例,类型为 *T,是指针类型。

示例:

package main

import "fmt"

type Command struct {
	Name    string    // 指令名称
	Var     *int      // 指令绑定的变量
	Comment string    // 指令的注释
}

func newCommand(name string, varRef *int, comment string) *Command {
	return &Command{
		Name:    name,
		Var:     varRef,
		Comment: comment,
	}
}

var version = 1
func main() {
	cmd := newCommand(
		"version",
		&version,
		"show version",
	)
	fmt.Println(cmd)
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

1.2 匿名结构体

匿名结构体没有类型名称,无须通过 type 关键字定义就可以直接使用。

ins := struct {
    // 匿名结构体字段定义
    字段1 字段类型1
    字段2 字段类型2}{
    // 字段值初始化
    初始化字段1: 字段1的值,
    初始化字段2: 字段2的值,}
1
2
3
4
5
6
7
8
9
10
11
  • 字段1、字段2……:结构体定义的字段名。
  • 初始化字段1、初始化字段2……:结构体初始化时的字段名,可选择性地对字段初始化。
  • 字段类型1、字段类型2……:结构体定义字段的类型。
  • 字段1的值、字段2的值……:结构体初始化字段的初始值。
package main
import (
	"fmt"
)
// 打印消息类型, 传入匿名结构体
func printMsgType(msg *struct {
	id   int
	data string
}) {
	// 使用动词%T打印msg的类型
	fmt.Printf("%T\n, msg:%v", msg,msg)
}
func main() {
	// 实例化一个匿名结构体
	msg := &struct {  // 定义部分
		id   int
		data string
	}{  // 值初始化部分
		1024,
		"hello",
	}
	printMsgType(msg)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

2. 方法

在Go语言中,结构体就像是类的一种简化形式,那么类的方法在哪里呢?

在Go语言中有一个概念,它和方法有着同样的名字,并且大体上意思相同,Go 方法是作用在接收器(receiver)上的一个函数,接收器是某种类型的变量,因此方法是一种特殊类型的函数。

接收器类型可以是(几乎)任何类型,不仅仅是结构体类型,任何类型都可以有方法,甚至可以是函数类型,可以是 int、bool、string 或数组的别名类型,但是接收器不能是一个接口类型,因为接口是一个抽象定义,而方法却是具体实现,如果这样做了就会引发一个编译错误invalid receiver type…

接收器也不能是一个指针类型,但是它可以是任何其他允许类型的指针。

一个类型加上它的方法等价于面向对象中的一个类

在Go语言中,类型的代码和绑定在它上面的方法的代码可以不放置在一起,它们可以存在不同的源文件中,唯一的要求是它们必须是同一个包的

类型 T(或 T)上的所有方法的集合叫做类型 T(或 T)的方法集。

在面向对象的语言中,类拥有的方法一般被理解为类可以做的事情。在Go语言中“方法”的概念与其他语言一致,只是Go语言建立的“接收器”强调方法的作用对象是接收器,也就是类实例,而函数没有作用对象。

为结构体添加方法:

需求:将物品放入背包

面向对象的写法:

​ 将背包做为一个对象,将物品放入背包的过程作为“方法”

package main

import "fmt"

type Bag struct {
	items []int
}
func (b *Bag) Insert(itemid int) {
	b.items = append(b.items, itemid)
}
func main() {
	b := new(Bag)
	b.Insert(1001)
	fmt.Println(b.items)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

(b*Bag) 表示接收器,即 Insert 作用的对象实例。每个方法只能有一个接收器

2.1 接收器

接收器的格式如下:

func (接收器变量 接收器类型) 方法名(参数列表) (返回参数) {
    函数体
}
1
2
3
  • 接收器变量:接收器中的参数变量名在命名时,官方建议使用接收器类型名的第一个小写字母,而不是 self、this 之类的命名。例如,Socket 类型的接收器变量应该命名为 s,Connector 类型的接收器变量应该命名为 c 等。
  • 接收器类型:接收器类型和参数类似,可以是指针类型和非指针类型。
  • 方法名、参数列表、返回参数:格式与函数定义一致。

接收器根据接收器的类型可以分为指针接收器非指针接收器,两种接收器在使用时会产生不同的效果,根据效果的不同,两种接收器会被用于不同性能和功能要求的代码中。

指针类型的接收器:

指针类型的接收器由一个结构体的指针组成,更接近于面向对象中的 this 或者 self。

由于指针的特性,调用方法时,修改接收器指针的任意成员变量,在方法结束后,修改都是有效的

示例:

使用结构体定义一个属性(Property),为属性添加 SetValue() 方法以封装设置属性的过程,通过属性的 Value() 方法可以重新获得属性的数值,使用属性时,通过 SetValue() 方法的调用,可以达成修改属性值的效果:

package main
import "fmt"
// 定义属性结构
type Property struct {
    value int  // 属性值
}
// 设置属性值
func (p *Property) SetValue(v int) {
    // 修改p的成员变量
    p.value = v
}
// 取属性值
func (p *Property) Value() int {
    return p.value
}
func main() {
    // 实例化属性
    p := new(Property)
    // 设置值
    p.SetValue(100)
    // 打印值
    fmt.Println(p.Value())
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

非指针类型的接收器:

当方法作用于非指针接收器时,Go语言会在代码运行时将接收器的值复制一份,在非指针接收器的方法中可以获取接收器的成员值,但修改后无效

点(Point)使用结构体描述时,为点添加 Add() 方法,这个方法不能修改 Point 的成员 X、Y 变量,而是在计算后返回新的 Point 对象,Point 属于小内存对象,在函数返回值的复制过程中可以极大地提高代码运行效率:

package main
import (
    "fmt"
)
// 定义点结构
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)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

在计算机中,小对象由于值复制时的速度较快,所以适合使用非指针接收器,大对象因为复制性能较低,适合使用指针接收器,在接收器和参数间传递时不进行复制,只是传递指针。

3. 二维矢量模拟玩家移动

在游戏中,一般使用二维矢量保存玩家的位置,使用矢量运算可以计算出玩家移动的位置,本例子中,首先实现二维矢量对象,接着构造玩家对象,最后使用矢量对象和玩家对象共同模拟玩家移动的过程。

实现二维矢量结构:

矢量是数学中的概念,二维矢量拥有两个方向的信息,同时可以进行加、减、乘(缩放)、距离、单位化等计算,在计算机中,使用拥有 X 和 Y 两个分量的 Vec2 结构体实现数学中二维向量的概念。

package main
import "math"
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) Vec2 {
    return Vec2{
        v.X - other.X,
        v.Y - other.Y,
    }
}
// 乘 缩放或者叫矢量乘法,是对矢量的每个分量乘上缩放比,Scale() 方法传入一个参数同时乘两个分量,表示这个缩放是一个等比缩放
func (v Vec2) Scale(s float32) Vec2 {
    return Vec2{v.X * s, v.Y * s}
}
// 距离 计算两个矢量的距离,math.Sqrt() 是开方函数,参数是 float64,在使用时需要转换,返回值也是 float64,需要转换回 float32
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}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

实现玩家对象:

玩家对象负责存储玩家的当前位置、目标位置和速度,使用 MoveTo() 方法为玩家设定移动的目标,使用 Update() 方法更新玩家位置,在 Update() 方法中,通过一系列的矢量计算获得玩家移动后的新位置。

  1. 使用矢量减法,将目标位置(targetPos)减去当前位置(currPos)即可计算出位于两个位置之间的新矢量
  2. 使用 Normalize() 方法将方向矢量变为模为 1 的单位化矢量,这里需要将矢量单位化后才能进行后续计算
  3. 获得方向后,将单位化方向矢量根据速度进行等比缩放,速度越快,速度数值越大,乘上方向后生成的矢量就越长(模很大)
  4. 将缩放后的方向添加到当前位置后形成新的位置
package main
type Player struct {
    currPos   Vec2    // 当前位置
    targetPos Vec2    // 目标位置
    speed     float32 // 移动速度
}
// 移动到某个点就是设置目标位置
//逻辑层通过这个函数告知玩家要去的目标位置,随后的移动过程由 Update() 方法负责
func (p *Player) MoveTo(v Vec2) {
    p.targetPos = v
}
// 获取当前的位置
func (p *Player) Pos() Vec2 {
    return p.currPos
}

//判断玩家是否到达目标点,玩家每次移动的半径就是速度(speed),因此,如果与目标点的距离小于速度,表示已经非常靠近目标,可以视为到达目标。
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,
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

处理移动逻辑:

将 Player 实例化后,设定玩家移动的最终目标点,之后开始进行移动的过程,这是一个不断更新位置的循环过程,每次检测玩家是否靠近目标点附近,如果还没有到达,则不断地更新位置,让玩家朝着目标点不停的修改当前位置,如下代码所示:

package main
import "fmt"

func main() {
	// 实例化玩家对象,并设速度为0.5
	p := NewPlayer(0.5)
	// 让玩家移动到3,1点
	p.MoveTo(Vec2{3, 1})
	// 如果没有到达就一直循环
	for !p.IsArrived() {
		// 更新玩家位置
		p.Update()
		// 打印每次移动后的玩家位置
		fmt.Println(p.Pos())
	}
	fmt.Printf("到达了:%v",p.Pos())
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

4. 给任意类型添加方法

Go语言可以对任何类型添加方法,给一种类型添加方法就像给结构体添加方法一样,因为结构体也是一种类型。

为基本类型添加方法:

在Go语言中,使用 type 关键字可以定义出新的自定义类型,之后就可以为自定义类型添加各种方法了。我们习惯于使用面向过程的方式判断一个值是否为 0,例如:

if  v == 0 {
    // v等于0
}
1
2
3

如果将 v 当做整型对象,那么判断 v 值就可以增加一个 IsZero() 方法,通过这个方法就可以判断 v 值是否为 0,例如:

if  v.IsZero() {
    // v等于0
}
1
2
3

为基本类型添加方法的详细实现流程如下:

package main
import (
    "fmt"
)
// 将int定义为MyInt类型
type MyInt int
// 为MyInt添加IsZero()方法
func (m MyInt) IsZero() bool {
    return m == 0
}
// 为MyInt添加Add()方法
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))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

5. 匿名字段

结构体可以包含一个或多个匿名(或内嵌)字段,即这些字段没有显式的名字,只有字段的类型是必须的,此时类型也就是字段的名字。

匿名字段本身可以是一个结构体类型,即结构体可以包含内嵌结构体。

Go语言中的继承是通过内嵌或组合来实现的,所以可以说,在Go语言中,相比较于继承,组合更受青睐。

package main

import "fmt"

type User struct {
    id   int
    name string
}

type Manager struct {
    User
}

func (self *User) ToString() string { // receiver = &(Manager.User)
    return fmt.Sprintf("User: %p, %v", self, self)
}

func main() {
    m := Manager{User{1, "Tom"}}
    fmt.Printf("Manager: %p\n", &m)
    fmt.Println(m.ToString())
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

类似于重写的功能:

package main

import "fmt"

type User struct {
    id   int
    name string
}

type Manager struct {
    User
    title string
}

func (self *User) ToString() string {
    return fmt.Sprintf("User: %p, %v", self, self)
}

func (self *Manager) ToString() string {
    return fmt.Sprintf("Manager: %p, %v", self, self)
}

func main() {
    m := Manager{User{1, "Tom"}, "Administrator"}

    fmt.Println(m.ToString())

    fmt.Println(m.User.ToString())
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29