数据类型
基础类型
类型 | 描述 |
---|---|
布尔型 | 布尔型的值只可以是 true 或者 false,示例 var b bool = true |
字符串类型 | 默认值为空字符串,使用双引号或反引号定义。字符串是一种值类型,且值不可变,即创建某个文本后将无法再次修改这个文本的内容 |
数字类型 | 整型 int 和浮点型 float32、float64,Go 语言支持整型和浮点型数字,并且支持复数,其中位的运算采用补码。 |
整型
类型 | 描述 |
---|---|
uint8 | 无符号8位整型(0 - 255) |
uint16 | 无符号16位整型(0 - 65535) |
uint32 | 无符号32位整型(0 - 4294967295) |
uint64 | 无符号64位整型(0 - 18446744073709551615) |
int8 | 有符号8位整型(-128 ~ 127) |
int16 | 有符号16位整型(-32768 ~ 32767) |
int32 | 有符号32位整型(-2147483648 ~ 2147483647) |
int64 | 有符号64位整型(-9223372036854775808 ~ 9223372036854775807) |
uintptr | 无符号整型,用于存放一个指针 |
int | 有符号32位或64位 |
uint | 无符号32位或64位 |
byte | 类似 uint8 |
rune | 类似 int32 |
大多数情况下,我们只需要 int 一种整型即可,它可以用于循环计数器(for 循环中控制循环次数的变量)、数组和切片的索引,以及任何通用目的的整型运算符,通常 int 类型的处理速度也是最快的。
byte 和 rune 可以表示字符类型
var b byte = 'a'
fmt.Println(b) // 97
Go语言中不允许将布尔型强制转换为整型,编译会报错
var n bool
fmt.Println(int(n))
// cannot convert n (type bool) to type int
浮点型
默认为0,声明一个没有指定数据类型的浮点数时,默认为float64类型
类型 | 描述 |
---|---|
float32 | 32位浮点数 |
float64 | 64位浮点数 |
complex64 | 32位实数和虚数 |
complex128 | 64位实数和虚数 |
array
数组是具有相同 唯一类型的一组已编号且长度固定的数据项序列;这种类型可以是任意的原始类型例如整型、字符串或者自定义类型。数组长度必须是一个常量表达式,并且必须是一个非负整数。数组长度也是数组类型的一部分,所以 [5] int 和 [10] int 是属于不同类型的。
声明的格式是var identifier [len]type
。例如:var arr1 [5]int
对索引项为 i 的数组元素赋值可以这么操作:arr[i] = value,所以数组是 可变的。
初始化赋值
package main
import "fmt"
func main() {
a, b, c := 1, 2, 3
// 第一种
arr1 := [5]int{a, b, c ,4, 5}
// 第二种,可以忽略数组中元素
arr2 := [5]int{1, 2, 3}
// 第三种
var arr3 = [...]int{5, 6, 7, 8}
// 第四种,key: value syntax,数组长度可以省略,默认长度为数组索引最大值
var arr4 = [5]int{3: 3, 4: 4}
// 输出: [0 0 0 3 4]
fmt.Printf("arr1-type: %T; arr2-type: %T; arr3-type: %T; arr4-type: %T", arr1, arr2, arr3, arr4)
// arr1-type: [5]int; arr2-type: [5]int; arr3-type: [4]int; arr4-type: [5]int
}
多维数组
Go语言中允许使用多维数组,因为数组属于值类型,所以多维数组的所有维度都会在创建时自动初始化零值,多维数组尤其适合管理具有父子关系或者与坐标系相关联的数据,例如:[3][5]int(第一维3个元素,第二维5个元素),[2][2][2]float64。
内部数组总是长度相同的。Go 语言的多维数组是矩形式的(唯一的例外是切片的数组)
声明并赋值 var array = [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}
package main
import "fmt"
const (
ONE = 3
TWO = 2
)
type pixel int
var screen [ONE][TWO]pixel
func main() {
for y := 0; y < TWO; y++ {
for x := 0; x < ONE; x++ {
screen[x][y] = pixel(x) // 此处需要用类型转换
}
}
fmt.Println(screen) // [[0 0] [1 1] [2 2]]
}
比较两个数组是否相等
如果两个数组类型相同(包括数组的长度,数组中元素的类型)的情况下,我们可以直接通过较运算符(==和!=)来判断两个数组是否相等,只有当两个数组的所有元素都是相等的时候数组才是相等的,不能比较两个类型不同的数组,否则程序将无法完成编译。
package main
import "fmt"
func main() {
a := [2]int{1, 2}
b := [...]int{1, 2}
c := [2]int{1, 3}
fmt.Println(a == b, a == c, b == c) // "true false false"
d := [3]int{1, 2}
fmt.Println(a == d) // 编译错误:无法比较 [2]int == [3]int
}
slice
切片(slice)是对数组的一个连续片段的引用,所以切片是一个引用类型,这个片段可以是整个数组,也可以是由起始和终止索引标识的一些项的子集,需要注意的是,终止索引标识的项不包括在切片内。
数组生成切片
切片默认指向一段连续内存区域,可以是数组,也可以是切片本身。
从连续内存区域生成切片是常见的操作,格式如下:
slice [开始位置 : 结束位置]
,切片结果不包括结束位置
从数组生成切片:
package main
import "fmt"
func main() {
var a = [5]int{1, 2, 3, 4, 5}
fmt.Println(a[1:3]) // [2 3]
}
从数组或切片生成新的切片拥有如下特性:
- 切片位置为:0 - len(数组);
- 取出元素不包含结束位置对应的索引;
- 当缺省开始位置时,表示从连续区域开头到结束位置;
- 当缺省结束位置时,表示从开始位置到整个连续区域末尾;
- 两者同时缺省时,与切片本身等效;
- 两者同时为 0 时,等效于空切片,一般用于切片复位。
直接声明新的切片
除了可以从原有的数组或者切片中生成切片外,也可以声明一个新的切片,每一种类型都可以拥有其切片类型,表示多个相同类型元素的连续集合,因此切片类型也可以被声明,切片类型声明格式如下:var name []Type,其中 name 表示切片的变量名,Type 表示切片对应的元素类型。
package main
import "fmt"
func main() {
// 声明字符串切片
var strList []string
// 声明整型切片
var numList []int
// 声明一个空切片
var numListEmpty = []int{}
// 输出3个切片
fmt.Println(strList, numList, numListEmpty) // [] [] []
// 输出3个切片大小
fmt.Println(len(strList), len(numList), len(numListEmpty)) // 0 0 0
// 切片判定空的结果
fmt.Println(strList == nil) // true
fmt.Println(numList == nil) // true
fmt.Println(numListEmpty == nil) // false
}
**切片是动态结构,只能与 nil 判定相等,不能互相判定相等。**声明新的切片后,可以使用 append() 函数向切片中添加元素。
使用 make() 函数构造切片
如果需要动态地创建一个切片,可以使用 make() 内建函数,格式如下:
make( []Type, size, cap )
其中 Type 是指切片的元素类型,size 指的是为这个类型分配多少个元素,cap 为预分配的元素数量,这个值设定后不影响 size,只是能提前分配空间,降低多次分配空间造成的性能问题。
package main
import "fmt"
func main() {
a := make([]int, 2)
b := make([]int, 2, 10)
fmt.Println(a, b) // [0 0] [0 0]
fmt.Println(len(a), len(b)) // 2 2
}
其中 a 和 b 均是预分配 2 个元素的切片,只是 b 的内部存储空间已经分配了 10 个,但实际使用了 2 个元素。
容量不会影响当前的元素个数,因此 a 和 b 取 len 都是 2。
使用 make() 函数生成的切片一定发生了内存分配操作,但给定开始与结束位置(包括切片复位)的切片只是将新的切片结构指向已经分配好的内存区域,设定开始与结束位置,不会发生内存分配操作。
append()
Go语言的内建函数 append() 可以为切片动态添加元素
package main
import "fmt"
func main() {
var a []int
a = append(a, 1) // 追加1个元素
a = append(a, 1, 2, 3) // 追加多个元素, 手写解包方式
a = append(a, []int{1,2,3}...) // 追加一个切片, 切片需要解包
fmt.Println(a) // [1 1 2 3 1 2 3]
}
除了在切片的尾部追加,我们还可以在切片的开头添加元素
package main
import "fmt"
func main() {
var a = []int{1,2,3}
a = append([]int{0}, a...) // 在开头添加1个元素
a = append([]int{-3,-2,-1}, a...) // 在开头添加1个切片
fmt.Println(a) // [-3 -2 -1 0 1 2 3]
}
在切片开头添加元素一般都会导致内存的重新分配,而且会导致已有元素全部被复制 1 次,因此,从切片的开头添加元素的性能要比从尾部追加元素的性能差很多。
从切片中删除元素
Go语言并没有对删除切片元素提供专用的语法或者接口,需要使用切片本身的特性来删除元素,根据要删除元素的位置有三种情况,分别是从开头位置删除、从中间位置删除和从尾部删除,其中删除切片尾部的元素速度最快。
删除头部元素
package main
import "fmt"
func main() {
slice := []int{1, 2, 3}
slice = slice[1:] // 删除开头1个元素
fmt.Println(slice) // [2 3]
}
也可以不移动数据指针,但是将后面的数据向开头移动,可以用 append 原地完成(所谓原地完成是指在原有的切片数据对应的内存区间内完成,不会导致内存空间结构的变化)
package main
import "fmt"
func main() {
slice := []int{1, 2, 3}
slice = append(slice[:0], slice[1:]...) // 删除开头1个元素
fmt.Println(slice) // [2 3]
}
删除尾部元素
package main
import "fmt"
func main() {
// 删除尾部一个元素
slice := []int{1, 2, 3, 4, 5, 6}
// slice = slice[:len(slice)-1]
slice = append(slice[:len(slice)-1], slice[:0]...)
fmt.Println(slice)
}
删除任意位置元素
package main
import "fmt"
func main() {
slice := []int{1, 2, 3, 4, 5, 6}
index := 3
slice = append(slice[:index], slice[index+1:]...)
fmt.Println(slice)
}
切片随机取出元素
package main
import (
"fmt"
"time"
"math/rand"
)
func main(){
unPatientIdList := []int{1, 2, 3, 4}
rand.Seed(time.Now().Unix())
randomNum := rand.Intn(len(unPatientIdList)) // 取出的范围0-3
fmt.Println(unPatientIdList[randomNum])
}
package main
import "fmt"
func main() {
a := []int{1, 2, 3, 4, 5}
b := a[1:4]
c := b[2:4]
fmt.Println(c) // [4 5]
}
Reslice
- Reslice时索引以被slice的切片为准
- 索引不可以超过被slice的切片的容量cap()值
- 索引越界不会导致底层数组的重新分配而是引发错误
Append
- 可以在slice 尾部追加元素
- 可以将一个slice追加在另一个slice尾部
- 如果最终长度未超过追加到slice的容量则返回原始slice
- 如果超过追加到的slice的容量则将重新分配数组并拷贝原始数据
map
Go语言中 map 是一种特殊的数据结构,一种元素对(pair)的无序集合,pair 对应一个 key(索引)和一个 value(值),所以这个结构也称为关联数组或字典,这是一种能够快速寻找值的理想结构,给定 key,就可以迅速找到对应的 value。
map 是引用类型,可以使用如下方式声明:
var mapname map[keyType]valueType
- mapname 为 map 变量名
- keyType 为键类型
- valueType 为键对应的值类型
在声明的时候不需要知道 map 的长度,因为 map 是可以动态增长的,未初始化的 map 的值是 nil,使用函数 len() 可以获取 map 中 pair 的数目。
映射的键可以是任何值。这个值的类型可以是内置的类型,也可以是结构类型,只要这个值可以使用==、!= 运算符做比较。切片、函数以及包含切片的结构类型这些类型由于具有引用语义,不能作为映射的键,使用这些类型会造成编译错误
package main
import "fmt"
func main() {
var map1 map[string]int
map1 = map[string]int{"one": 1, "two": 2}
var map2 = make(map[string]int)
map2 = map1
// map也是一种引用类型,如果两个map同时指向一个底层,那么一个改变,另一个也相应改变。
map2["two"] = 3
fmt.Printf("Map1 is: %v\n", map1) // Map1 is: map[one:1 two:3]
fmt.Printf("Map2 is: %v\n", map2) // Map2 is: map[one:1 two:3]
}
注意:可以使用 make(),但不能使用 new() 来构造 map,如果错误的使用 new() 分配了一个引用对象,会获得一个空引用的指针,相当于声明了一个未初始化的变量并且取了它的地址:
map2 := new(map[string]int)
接下来当我们调用 map2["two"] = 3
的时候,编译器会报错:
invalid operation: map2["two"] (type *map[string]int does not support indexing)
map遍历
package main
import "fmt"
func main() {
var map1 map[string]int
map1 = map[string]int{"one": 1, "two": 2, "three": 3}
for key, val := range map1{
fmt.Printf("key is: %v --- val is: %v\n", key, val)
}
}
只遍历值可以使用_
改为匿名变量的形式:
for _, val := range map{
只遍历键可以直接忽略值:
for key := range map{
map 是无序(存入的值和取出的值顺序不一定一致)的如果需要特定顺序的遍历结果,正确的做法是先排序
package main
import (
"fmt"
"sort"
)
func main() {
var map1 map[string]int
map1 = map[string]int{"one": 1, "two": 2, "three": 3}
map1["a"] = 0
map1["p"] = 4
var slice1 []string
for key := range map1{
slice1 = append(slice1, key)
}
sort.Strings(slice1)
fmt.Println(slice1) // [a one p three two]
}
删除元素
使用 delete() 内建函数从 map 中删除一组键值对,delete() 函数的格式如下:
delete(map, key)
package main
import (
"fmt"
)
func main() {
var map1 map[string]int
map1 = map[string]int{"one": 1, "two": 2, "three": 3}
delete(map1, "two")
fmt.Printf("%v", map1) // map[one:1 three:3]
}
判断键是否存在
package main
import (
"fmt"
)
func main() {
var map1 map[string]int
map1 = map[string]int{"one": 1, "two": 2, "three": 3}
value, exist := map1["one"]
// 键存在 exist 为true, 否则为 false
fmt.Println(exist) // true
if exist{
fmt.Printf("%v", value)
}
}
struct
定义
Go语言可以通过自定义的方式形成新的类型,结构体就是这些类型中的一种复合类型,结构体是由零个或多个任意类型的值聚合成的实体,每个值都可以称为结构体的成员。
结构体成员也可以称为“字段”,这些字段有以下特性:
- 字段拥有自己的类型和值;
- 字段名必须唯一;
- 字段的类型也可以是结构体,甚至是字段所在结构体的类型。
使用关键字 type 可以将各种基本类型定义为自定义类型,基本类型包括整型、字符串、布尔等。结构体是一种复合的基本类型,通过 type 定义为自定义类型后,使结构体更便于使用。
type 类型名 struct{
字段 类型
}
例如:
type Person struct{
name string
age int
}
同类型的变量也可以写在一行
type Person struct{
name, email string
age int
}
实例化
结构体的定义只是一种内存布局的描述,只有当结构体实例化时,才会真正地分配内存,因此必须在定义结构体并实例化后才能使用结构体的字段。
实例化就是根据结构体定义的格式创建一份与格式一致的内存区域,结构体实例与实例间的内存是完全独立的。
Go语言可以通过多种方式实例化结构体,根据实际需要可以选用不同的写法。
var 实例化方式
package main
import "fmt"
type Person struct{
name string
age int
}
func main() {
var tom = Person
tom.name = "Tom"
tom.age = 18
// var tom = Person{"Tom", 18} 可以省略字段名,但是顺序必须一一对应
fmt.Println(tom) // {Tom 18}
}
new 实例化方式
package main
import "fmt"
type Person struct{
name string
age int
}
func main() {
tom := new(Person)
tom.name = "Tom"
tom.age = 18
fmt.Println(tom) // &{Tom 18}
}
结构体初始化
使用“键值对”初始化结构体
type Person struct{
name string
age int
}
user := Person{name: "Tony", age: 18}
省略键初始化结构体
type Person struct{
name string
age int
}
user := Person{"Tony", 18}
初始化匿名结构体
匿名结构体的初始化写法由结构体定义和键值对初始化两部分组成,结构体定义时没有结构体类型名,只有字段和类型定义,键值对初始化部分由可选的多个键值对组成:
init := struct {
// 匿名结构体字段定义
字段 字段类型
…
}{
// 字段值初始化,注意每行结尾必须有逗号
初始化字段: 字段的值,
…
}
package main
import "fmt"
func main() {
init := struct{
name string
age int
}{
name: "Tom",
age: 18,
}
fmt.Printf("%v", init) // {Tom 18}
}
匿名字段
Go语言支持只提供类型,而不写字段名的方式,也就是匿名字段,或称为嵌入字段。
当匿名字段是一个struct的时候,那么这个struct所拥有的全部字段都被隐式地引入了当前定义的这个struct。
package main
import "fmt"
type Person struct{
name string
age int
}
type Tom struct{
Person
height int
}
func main() {
tom := Tom{Person{"Tom", 18}, 180}
fmt.Printf("%v", tom.name) // Tom
// 通过Person作为字段名访问
fmt.Printf("%v", tom.Person.name) // Tom
}
自定义类型和内置类型等都可以作为匿名字段,而且还可以在相应的字段是进行函数操作:
package main
import "fmt"
type Person struct{
name string
age int
}
// 类型定义
type Skills []string
type Tom struct{
Person // 匿名字段,struct
Skills // 匿名字段,自定义类型 string slice
height int
int // 内置类型作为匿名字段
}
func main() {
tom := Tom{Person:Person{"Tom", 18}, Skills: []string{"computer", "excel"}, height: 180}
fmt.Printf("his name is:%s\n", tom.name) // he name is:Tom
// 修改 skill 字段
tom.Skills = append(tom.Skills, "Python")
fmt.Printf("his skills are:%v\n", tom.Skills) // he skills are:[computer excel Python]
// 修改匿名内置类型
tom.int = 50
fmt.Printf("%d", tom.int) // 50
}
如果 Person 里有个 phone 字段,而 Tom 里也有个 phone 字段,当通过 tom.phone 访问,会优先访问最外层的字段
这样就允许我们去重载通过匿名字段继承的一些字段,当然如果我们想访问重载后对应匿名类型里面的字段,可以通过匿名字段名来访问。
package main
import "fmt"
type Person struct{
name , phone string
age int
}
type Tom struct{
Person // 匿名字段,struct
phone string
}
func main() {
tom := Tom{Person:Person{"Tom", "xxx", 18}, phone: "xxxxxx"}
fmt.Printf("his phone is:%s\n", tom.phone) // his phone is:xxxxxx
fmt.Printf("his phone is:%s\n", tom.Person.phone) // his phone is:xxx
}
interface
定义
interface是一组method的组合,我们通过interface来定义对象的一组行为。
接口声明形式:
type 接口类型名 interface{
方法名1( 参数列表1 ) 返回值列表1
方法名2( 参数列表2 ) 返回值列表2
…
}
- 接口类型名:使用 type 将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加 er,如有写操作的接口叫 Writer,有字符串功能的接口叫 Stringer,有关闭功能的接口叫 Closer 等。
- 方法名:当方法名首字母是大写时,且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
- 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以被忽略,例如:
type writer interface{
Write([]byte) error
}
实现
接口定义后,需要实现接口,调用方才能正确编译通过并使用接口。接口的实现需要遵循两条规则才能让接口可用。
接口被实现的条件一:接口的方法与实现接口的类型方法格式一致
在类型中添加与接口签名一致的方法就可以实现该方法。签名包括方法中的名称、参数列表、返回参数列表。也就是说,只要实现接口类型中的方法的名称、参数列表、返回参数列表中的任意一项与接口要实现的方法不一致,那么接口的这个方法就不会被实现。
package main
import (
"fmt"
)
// 定义一个数据写入器
type DataWriter interface {
WriteData(data interface{}) error
}
// 定义文件结构,用于实现DataWriter
type file struct {
}
// 实现DataWriter接口的WriteData方法
func (d *file) WriteData(data interface{}) error {
// 模拟写入数据
fmt.Println("WriteData:", data)
return nil
}
func main() {
// 实例化file
f := new(file)
// 声明一个DataWriter的接口
var writer DataWriter
// 将接口赋值f,也就是*file类型
writer = f
// 使用DataWriter接口进行数据写入
writer.WriteData("data")
}
接口被实现的条件二:接口中所有方法均被实现
当一个接口中有多个方法时,只有这些方法都被实现了,接口才能被正确编译并使用。
为 DataWriter中 添加一个方法:
// 定义一个数据写入器
type DataWriter interface {
WriteData(data interface{}) error
// 能否写入
CanWrite() bool
}
编译报错
cannot use f (type *file) as type DataWriter in assignment:
*file does not implement DataWriter (missing CanWrite method)
需要在 file 中实现 CanWrite() 方法才能正常使用 DataWriter()。
一个接口的方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。也就是说,使用者并不关心某个接口的方法是通过一个类型完全实现的,还是通过多个结构嵌入到一个结构体中拼凑起来共同实现的。
// 一个服务需要满足能够开启和写日志的功能
type Service interface {
Start() // 开启服务
Log(string) // 日志输出
}
// 日志器
type Logger struct {
}
// 实现Service的Log()方法
func (g *Logger) Log(l string) {
}
// 游戏服务
type GameService struct {
Logger // 嵌入日志器
}
// 实现Service的Start()方法
func (g *GameService) Start() {
}
channel
定义
interface是一组method的组合,我们通过interface来定义对象的一组行为。
接口声明形式:
type 接口类型名 interface{
方法名1( 参数列表1 ) 返回值列表1
方法名2( 参数列表2 ) 返回值列表2
…
}
- 接口类型名:使用 type 将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加 er,如有写操作的接口叫 Writer,有字符串功能的接口叫 Stringer,有关闭功能的接口叫 Closer 等。
- 方法名:当方法名首字母是大写时,且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
- 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以被忽略,例如:
type writer interface{
Write([]byte) error
}
实现
接口定义后,需要实现接口,调用方才能正确编译通过并使用接口。接口的实现需要遵循两条规则才能让接口可用。
接口被实现的条件一:接口的方法与实现接口的类型方法格式一致
在类型中添加与接口签名一致的方法就可以实现该方法。签名包括方法中的名称、参数列表、返回参数列表。也就是说,只要实现接口类型中的方法的名称、参数列表、返回参数列表中的任意一项与接口要实现的方法不一致,那么接口的这个方法就不会被实现。
package main
import (
"fmt"
)
// 定义一个数据写入器
type DataWriter interface {
WriteData(data interface{}) error
}
// 定义文件结构,用于实现DataWriter
type file struct {
}
// 实现DataWriter接口的WriteData方法
func (d *file) WriteData(data interface{}) error {
// 模拟写入数据
fmt.Println("WriteData:", data)
return nil
}
func main() {
// 实例化file
f := new(file)
// 声明一个DataWriter的接口
var writer DataWriter
// 将接口赋值f,也就是*file类型
writer = f
// 使用DataWriter接口进行数据写入
writer.WriteData("data")
}
接口被实现的条件二:接口中所有方法均被实现
当一个接口中有多个方法时,只有这些方法都被实现了,接口才能被正确编译并使用。
为 DataWriter中 添加一个方法:
// 定义一个数据写入器
type DataWriter interface {
WriteData(data interface{}) error
// 能否写入
CanWrite() bool
}
编译报错
cannot use f (type *file) as type DataWriter in assignment:
*file does not implement DataWriter (missing CanWrite method)
需要在 file 中实现 CanWrite() 方法才能正常使用 DataWriter()。
一个接口的方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。也就是说,使用者并不关心某个接口的方法是通过一个类型完全实现的,还是通过多个结构嵌入到一个结构体中拼凑起来共同实现的。
// 一个服务需要满足能够开启和写日志的功能
type Service interface {
Start() // 开启服务
Log(string) // 日志输出
}
// 日志器
type Logger struct {
}
// 实现Service的Log()方法
func (g *Logger) Log(l string) {
}
// 游戏服务
type GameService struct {
Logger // 嵌入日志器
}
// 实现Service的Start()方法
func (g *GameService) Start() {
}
指针
什么是指针
程序运行时的数据是存放在内存中的,而内存会被抽象为一系列具有连续编号的存储空间,那么每一个存储在内存中的数据都会有一个编号,这个编号就是内存地址。有了这个内存地址就可以找到这个内存中存储的数据,而内存地址可以被赋值给一个指针。
可以总结为:在编程语言中,指针是一种数据类型,用来存储一个内存地址,该地址指向存储在该内存中的对象。这个对象可以是字符串、整数、函数或者你自定义的结构体。
指针的声明和定义
在 Go 语言中,获取一个变量的指针非常容易,使用取地址符 & 就可以,比如下面的例子:
func main() {
strVal := "PHP is the best language in the world!"
strPtr := &strPtr//取地址
fmt.Println("strPtrr变量的值为:",strPtrr)
fmt.Println("strPtr变量的内存地址为:",strPtr)
}
指针类型非常廉价,只占用 4 个或者 8 个字节的内存大小。
以上示例中 strPtr 指针的类型是 *string,用于指向 string 类型的数据。在 Go 语言中使用类型名称前加 *
的方式,即可表示一个对应的指针类型。比如 int 类型的指针类型是 *int,float64 类型的指针类型是 *float64,自定义结构体 A 的指针类型是 *A。总之,指针类型就是在对应的类型前加 * 号。
此外,除了可以通过简短声明的方式声明一个指针类型的变量外,也可以使用 var 关键字声明, var intP *int
通过 var 声明的指针变量是不能直接赋值和取值的,因为这时候它仅仅是个变量,还没有对应的内存地址,它的值是 nil。
和普通类型不一样的是,指针类型还可以通过内置的 new 函数来声明,如下所示:
intP := new(int)
内置的 new 函数有一个参数,可以传递类型给它。它会返回对应的指针类型,比如上述示例中会返回一个 *int 类型的 intP。
指针的操作
在 Go 语言中指针的操作无非是两种:一种是获取指针指向的值,一种是修改指针指向的值。
要获取指针指向的值,只需要在指针变量前加 * 号即可:
strVal := *strPtr
fmt.Println("strPtr指针指向的值为:", strVal)
修改指针指向的值也非常简单
*strPtr = "hello world" //修改指针指向的值
fmt.Println("strPtr指针指向的值为:",*strPtr) // hello world
fmt.Println("strPtr变量的值为:", str) // hello world
通过打印结果可以看到,不光 strPtr 指针指向的值被改变了,变量 strVal 的值也被改变了,这就是指针的作用。因为变量 strVal 存储数据的内存就是指针 strPtr 指向的内存,这块内存被 strPtr 修改后,变量 strVal 的值也被修改了。
通过 var 关键字直接定义的指针变量是不能进行赋值操作的,因为它的值为 nil,也就是还没有指向的内存地址。比如下面的示例:
var intP *int
*intP =10
运行的时候会提示 invalid memory address or nil pointer dereference
。这时候该怎么办呢?其实只需要通过 new 函数给它分配一块内存就可以了,如下所示:
var intPtr *int = new(int)
或者
//intPtr := new(int)
指针接收者
对于是否使用指针类型作为接收者,有以下几点参考:
- 如果接收者类型是 map、slice、channel 这类引用类型,不使用指针;
- 如果需要修改接收者,那么需要使用指针;
- 如果接收者是比较大的类型,可以考虑使用指针,因为内存拷贝廉价,所以效率高。
什么情况下使用指针
从以上指针的详细分析中,可以总结出指针的两大好处:
- 可以修改指向数据的值;
- 在变量赋值,参数传值的时候可以节省内存。
不过 Go 语言作为一种高级语言,在指针的使用上还是比较克制的。它在设计的时候就对指针进行了诸多限制,比如指针不能进行运行,也不能获取常量的指针。所以在思考是否使用时,我们也要保持克制的心态。
使用指针的建议:
- 不要对 map、slice、channel 这类引用类型使用指针;
- 如果需要修改方法接收者内部的数据或者状态时,需要使用指针;
- 如果需要修改参数的值或者内部数据时,也需要使用指针类型的参数;
- 如果是比较大的结构体,每次参数传递或者调用方法都要内存拷贝,内存占用多,这时候可以考虑使用指针;
- 像 int、bool 这样的小数据类型没必要使用指针;
- 如果需要并发安全,则尽可能地不要使用指针,使用指针一定要保证并发安全;
- 指针最好不要嵌套,也就是不要使用一个指向指针的指针,虽然 Go 语言允许这么做,但是这会使你的代码变得异常复杂。