Golang 学习笔记 - 数据类型

基础类型

类型描述
布尔型布尔型的值只可以是 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类型

类型描述
float3232位浮点数
float6464位浮点数
complex6432位实数和虚数
complex12864位实数和虚数

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)

指针接收者

对于是否使用指针类型作为接收者,有以下几点参考:

  1. 如果接收者类型是 map、slice、channel 这类引用类型,不使用指针;
  2. 如果需要修改接收者,那么需要使用指针;
  3. 如果接收者是比较大的类型,可以考虑使用指针,因为内存拷贝廉价,所以效率高。

什么情况下使用指针

从以上指针的详细分析中,可以总结出指针的两大好处:

  1. 可以修改指向数据的值;
  2. 在变量赋值,参数传值的时候可以节省内存。

不过 Go 语言作为一种高级语言,在指针的使用上还是比较克制的。它在设计的时候就对指针进行了诸多限制,比如指针不能进行运行,也不能获取常量的指针。所以在思考是否使用时,我们也要保持克制的心态。

使用指针的建议:

  1. 不要对 map、slice、channel 这类引用类型使用指针;
  2. 如果需要修改方法接收者内部的数据或者状态时,需要使用指针;
  3. 如果需要修改参数的值或者内部数据时,也需要使用指针类型的参数;
  4. 如果是比较大的结构体,每次参数传递或者调用方法都要内存拷贝,内存占用多,这时候可以考虑使用指针;
  5. 像 int、bool 这样的小数据类型没必要使用指针;
  6. 如果需要并发安全,则尽可能地不要使用指针,使用指针一定要保证并发安全;
  7. 指针最好不要嵌套,也就是不要使用一个指向指针的指针,虽然 Go 语言允许这么做,但是这会使你的代码变得异常复杂。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值