# 2. GO语言基础

# 2.1 基本类型以及关键字和标识符

# 2.1.1 基本类型介绍

Go语言中有丰富的数据类型,除了基本的整型、浮点型、布尔型、字符串外,还有切片、结构体、函数、map、通道(channel)等。常用基本类型如下:

类型 长度(字节) 默认值 说明
bool 1 false 布尔类型
byte 1 0 别名unit8,代表ASCII 码的一个字符
rune 4 0 int32别名
int,uint 4或8 0 整数类型,整数大小会在 32bit 或 64bit 之间变化
int8, uint8 1 0 -128 ~ 127, 0 ~ 255,byte是uint8 的别名
int16, uint16 2 0 -32768 ~ 32767, 0 ~ 65535
int32, uint32 4 0 -21亿~ 21亿, 0 ~ 42亿,rune是int32 的别名
int64, uint64 8 0
float32 4 0.0
float64 8 0.0
complex64 8
complex128 16
uintptr 4或8 以存储指针的 uint32 或 uint64 整数
array 数组,属于值类型
struct 结构体,属于值类型
string “” UTF-8字符串
slice nil 切片,属于引用类型
map nil map,属于引用类型
channel nil 管道,属于引用类型
interface nil 接口
function nil 函数

# 2.1.2 整形

Go语言同时提供了有符号和无符号的整数类型,其中包括 int8、int16、int32 和 int64 四种大小截然不同的有符号整数类型,与此对应的是 uint8、uint16、uint32 和 uint64 四种无符号整数类型。

此外还有两种整数类型 int 和 uint,它们分别对应特定 CPU 平台的字长(机器字大小),其中 int 表示有符号整数,应用最为广泛,uint 表示无符号整数。实际开发中由于编译器和计算机硬件的不同,int 和 uint 所能表示的整数大小会在 32bit 或 64bit 之间变化。

用来表示 Unicode 字符的 rune 类型和 int32 类型是等价的,通常用于表示一个 Unicode 码点。这两个名称可以互换使用。同样,byte 和 uint8 也是等价类型,byte 类型一般用于强调数值是一个原始的数据而不是一个小的整数。

最后,还有一种无符号的整数类型 uintptr,在32平台下为4字节,在64位平台下是8字节,大小但是足以容纳指针。uintptr 类型只有在底层编程时才需要,特别是Go语言和C语言函数库或操作系统接口相交互的地方。

# 2.1.3 浮点型

Go语言提供了两种精度的浮点数 float32 和 float64;浮点数类型的取值范围可以从很微小到很巨大。浮点数取值范围的极限值可以在 math 包中找到:

  • 常量 math.MaxFloat32 表示 float32 能取到的最大数值,大约是 3.4e38;
  • 常量 math.MaxFloat64 表示 float64 能取到的最大数值,大约是 1.8e308;
  • float32 和 float64 能表示的最小值分别为 1.4e-45 和 4.9e-324

# 2.1.4 布尔类型

布尔类型的值只有两种:true 或 false, 布尔类型默认值为 false。

  • Go语言中不允许将整型强制转换为布尔型。
  • 布尔型无法参与数值运算,也无法与其他类型进行转换。

# 2.1.5 复数类型

复数是由两个浮点数表示的,其中一个表示实部(real),一个表示虚部(imag)。

Go语言中复数的类型有两种,分别是 complex128(64 位实数和虚数)和 complex64(32 位实数和虚数),其中 complex128 为复数的默认类型。

复数的值由三部分组成 RE + IMi,其中 RE 是实数部分,IM 是虚数部分,RE 和 IM 均为 float 类型,而最后的 i 是虚数单位。

声明复数的语法格式如下所示:

var name complex128 = complex(x, y)

其中 name 为复数的变量名,complex128 为复数的类型,“=”后面的 complex 为Go语言的内置函数用于为复数赋值,x、y 分别表示构成该复数的两个 float64 类型的数值,x 为实部,y 为虚部。

# 2.1.6 字符串类型

一个字符串是一个不可改变的字节序列,字符串可以包含任意的数据,但是通常是用来包含可读的文本,字符串是 UTF-8 字符的一个序列(当字符为 ASCII 码表上的字符时则占用 1 个字节,其它字符根据需要占用 2-4 个字节)。

字符串是一种值类型,且值不可变,即创建某个文本后将无法再次修改这个文本的内容,更深入地讲,字符串是字节的定长数组。

在Go语言中使用双引号书写字符串的方式是字符串常见表达方式之一, 被称为字符串字面量(string literal),这种双引号字面量不能跨行,如果想要在源码中嵌入一个多行字符串时,就必须使用 ` 反引号, 字符串中可以使用转义字符来实现换行、缩进等效果,常用的转义字符包括如下:

转义字符 说明
\r 回车符
\n 换行符
\t tab键
\U or \u Unicode 字符
\\ 反斜杠本身
\` 单引号
\" 双引号

字符串所占的字节长度可以通过函数 len() 来获取,例如 len(str)。

字符串的内容(纯字节)可以通过标准索引法来获取,在方括号[ ]内写入索引,索引从 0 开始计数:

  • 字符串 str 的第 1 个字节:str[0]
  • 第 i 个字节:str[i - 1]
  • 最后 1 个字节:str[len(str)-1]

需要注意的是,这种转换方案只对纯 ASCII 码的字符串有效。

字符串常用得操作方放如下表:

方法 说明
len(str) 获取长度
+ 字符串拼接
strings.Split 字符串分隔为数组
strings.Contains 字符串判断是否包含特定值
strings.HasPrefix,strings.HasSuffix 前缀/后缀判断
strings.Index(),strings.LastIndex() 子串出现的位置
strings.Join(a[]string, sep string) join操作

字符串类型在业务中的应用可以说是最广泛的,后续讲单独讲述字符串得相关操作。

# 2.1.7 语言字符类型(byte和rune)

字符串中的每一个元素叫做“字符”,在遍历或者单个获取字符串元素时可以获得字符。

Go语言的字符有以下两种:

  • 一种是 uint8 类型,或者叫 byte 型,代表了 ASCII 码的一个字符。
  • 另一种是 rune 类型,代表一个 UTF-8 字符,当需要处理中文、日文或者其他复合字符时,则需要用到 rune 类型。rune 类型等价于 int32 类型。

byte 类型是 uint8 的别名,对于只占用 1 个字节的传统 ASCII 编码的字符来说,完全没有问题,例如 var ch byte = 'A',字符使用单引号括起来。

在 ASCII 码表中,A 的值是 65,使用 16 进制表示则为 41,所以下面的写法是等效的:

var ch byte = 65 或 var ch byte = '\x41' //(\x 总是紧跟着长度为 2 的 16 进制数)

Go语言同样支持 Unicode(UTF-8),因此字符同样称为 Unicode 代码点或者 runes,并在内存中使用 int 来表示。在文档中,一般使用格式 U+hhhh 来表示,其中 h 表示一个 16 进制数。

在书写 Unicode 字符时,需要在 16 进制数之前加上前缀\u或者\U。因为 Unicode 至少占用 2 个字节,所以我们使用 int16 或者 int 类型来表示。如果需要使用到 4 字节,则使用\u前缀,如果需要使用到 8 个字节,则使用\U前缀。

var ch int = '\u0041'
var ch2 int = '\u03B2'
var ch3 int = '\U00101234'
fmt.Printf("%d - %d - %d\n", ch, ch2, ch3) // integer
fmt.Printf("%c - %c - %c\n", ch, ch2, ch3) // character
fmt.Printf("%X - %X - %X\n", ch, ch2, ch3) // UTF-8 bytes
fmt.Printf("%U - %U - %U", ch, ch2, ch3)   // UTF-8 code point

# 2.1.8 关键字

关键字即是被Go语言赋予了特殊含义的单词,也可以称为保留字。

Go语言中的关键字一共有 25 个:

    break        default      func         interface    select
    case         defer        go           map          struct
    chan         else         goto         package      switch
    const        fallthrough  if           range        type
    continue     for          import       return       var

# 2.1.9 标识符

Go语言中还存在着一些特殊的标识符,叫做预定义标识符,如下表所示:

append bool byte cap close complex complex64 complex128 uint16
copy false float32 float64 imag int int8 int16 uint32
int32 int64 iota len make new nil panic uint64
print println real recover string true uint uint8 uintptr

# 2.2 运算符

运算符是用来在程序运行时执行数学或逻辑运算的,在Go语言中,一个表达式可以包含多个运算符,当表达式中存在多个运算符时,就会遇到优先级的问题,此时应该先处理哪个运算符呢?这个就由Go语言运算符的优先级来决定的。

所谓优先级,就是当多个运算符出现在同一个表达式中时,先执行哪个运算符。Go语言运算符优先级和结合性一览表如下:

优先级 分类 运算符 结合性
1 逗号运算符 , 从左到右
2 赋值运算符 =、+=、-=、*=、/=、 %=、 >=、 <<=、&=、^=、|= 从右到左
3 逻辑或 || 从左到右
4 逻辑与 && 从左到右
5 按位或 | 从左到右
6 按位异或 ^ 从左到右
7 按位与 & 从左到右
8 相等/不等 ==、!= 从左到右
9 关系运算符 <、<=、>、>= 从左到右
10 位移运算符 <<、>> 从左到右
11 加法/减法 +、- 从左到右
12 乘法/除法/取余 *(乘号)、/、% 从左到右
13 单目运算符 !、*(指针)、& 、++、--、+(正号)、-(负号) 从右到左
14 后缀运算符 ( )、[ ]、-> 从左到右

注意:优先级值越大,表示优先级越高

# 2.3 变量与常量

# 2.3.1 变量声明

Go语言是静态类型语言,因此变量(variable)是有明确类型的,编译器也会检查变量类型的正确性。需要注意的是:Go语言的变量声明后如果在程序中未使用会抛出异常,所以一定要声明使用才行。

标准命名格式

Go语言的变量声明的标准格式为:

var 变量名 变量类型

变量声明以关键字 var 开头,后置变量类型,行尾无须分号。

示例:

var name string //定义类型为string的name变量
var age int //定义类型为int的age变量
var a *int  //定义类型为int指针的a变量

批量命令格式

如果觉得一个一个变量命名很麻烦,可以用懒人的批量命令方式,示例:

var (
    a int
    b string
    c float32
    d []float64 //定义d为float64类型的切片
    e func() bool //定义e为返回为布尔型的函数
    f struct{    // 定义结构体
        x int
    }
)

简短模式

除 var 关键字外,还可使用更加简短的变量定义和初始化语法,语法如下:

命名 := 表达式(或者值)

需要注意的是,简短模式(short variable declaration)有以下限制:

  • 定义变量,同时显式初始化。
  • 不能提供数据类型。
  • 只能用在函数内部。

示例:

func main(){
    x := 1
    y := "hello"
    a := false
}

因为简洁和灵活的特点,简短变量声明被广泛用于大部分的局部变量的声明和初始化。

# 2.3.2 变量初始化

Go语言在声明变量时,会自动赋予变量一个默认值,对应的内存区域进行初始化操作。每个变量会初始化其类型的默认值,默认值可以参考 2.1.1章节的表格。

初始化标准格式

变量初始化标准格式:

var 变量名 类型 = 表达式

示例:

var age int = 10
var name string = "LeiLei"
var female bool = false

在标准格式的基础上,也可以将类型省略,编译器会尝试根据等号右边的表达式推导 变量的类型,如下:

var age = 10
var name = "LeiLei"
var female = false

简短模式

变量声明中的也提到简短模式,两者是一样的概念,例如:

age := 10

这是Go语言的推导声明写法,编译器会自动根据右值类型推断出左值的对应类型, 由于使用了:=,而不是赋值的=,因此推导声明写法的左值变量必须是没有定义过的变量。若定义过,将会发生编译错误。

如果 age 已经被声明过,但依然使用:=时编译器会报错,代码如下:

var age int
age := 10

编译报错如下:

no new variables on left side of :=

但在多个短变量声明和赋值中,至少有一个新声明的变量出现在左值中,即便其他变量名可能是重复声明的,编译器也不会报错,示例:

conn, err := net.Dial("tcp", "127.0.0.1:8080")
conn2, err := net.Dial("tcp", "127.0.0.1:8080")

上述代码虽然err变量重复定义,但不会编译报错。

# 2.3.3 多变量同时赋值

多变量同时赋值大大简化了代码的行量以及阅读的复杂度,例如常规写法我们定义三个变量如下:

a := 1
b := 2
c := 3

采取多变量同时赋值,则可以简化为一行,代码如下:

a,b,c := 1,2,3

除了上述的简化功能外,多变量同时赋值在 变量交换 的操作中,也有意想不到的奇效,大大简化了代码逻辑,常规变量交换写法如下:

var t int // 临时变量t
a := 1
b := 2
t = a
a = b
b = t
fmt.Println(a, b)

上面代码是常规的变量交换写法,需要申请一个临时变量t进行转存,但使用多变量赋值就可以避免这个问题,代码如下:

a := 1
b := 2
a, b = b, a
fmt.Println(a, b)

多重赋值时,变量的左值和右值按从左到右的顺序赋值。

# 2.3.4 匿名变量

在使用多重赋值时,如果想要忽略某个值,可以使用匿名变量(anonymous variable)。 匿名变量用一个下划线 _ 表示, _ 本身就是一个特殊的标识符,被称为空白标识符。它可以像其他标识符那样用于变量的声明或赋值(任何类型都可以赋值给它),但任何赋给这个标识符的值都将被抛弃,因此这些值不能在后续的代码中使用,也不可以使用这个标识符作为变量对其它变量进行赋值或运算。

匿名变量不占用内存空间,不会分配内存。匿名变量与匿名变量之间也不会因为多次声明而无法使用。

使用示例:

func GetData() (int, int) {
    return 1, 2
}

func main(){
    a, _ := GetData()
    _, b := GetData()
    fmt.Println(a, b)
}

# 2.3.5 常量

Go语言中,常量定义使用关键字 const,常量一般用于存储不会经常改变的数据,常量在编译时候被创建,并且类型只能是 布尔型 数字型(整数 浮点数 复数)和字符串,由于编译时的限制,定义常量的表达式必须为能被编译器求值的常量表达式。

常量定义标准语法:

const 常量名 常量类型 = 值

示例: const a int = 5 当然也可以用隐式的写法: const a = 5

还有一点和变量声明一样,可以批量声明多个常量:

const (
   pi = 3.1415926
    e = 2.7182818
)

所有常量的运算都可以在编译期完成,这样不仅可以减少运行时的工作,也方便其他代码的编译优化。

所以常量的值必须是能够在编译时就能够确定的,可以在其赋值表达式中涉及计算过程,但是所有用于计算的值必须在编译期间就能获得。以下是错误的定义方式:

const c = getNumber()

iota 常量生成器

iota是一个古希腊字母 (opens new window). 在golang中表示常量计数器, 只能在常量的表达式中使用。使用iota能简化定义,在定义枚举时很有用 。

使用的规则如下:

  1. 每当const出现时, 都会使iota初始化为0.
  2. const中每新增一行常量声明将使iota计数一次.

示例1如下:

	const a0 = iota
	const (
		a1 = iota
		a2
		a3 = iota
		a4
		a5
	)
	fmt.Println(a0, a1, a2, a3, a4, a5)

编译输出: 0 0 1 2 3 4

示例2如下:

	const (
		Apple, Banana = iota * 10, iota+10
		Cherimoya,Durian
		Elderberry,Fig
	)
	fmt.Printf("%d-%d , %d-%d, %d-%d", Apple, Banana, Cherimoya, Durian, Elderberry, Fig)

编译输出: 0-10 , 10-11, 20-12

# 2.4 标准输入输出

# 2.4.1 标准输入

  • fmt.Scan/fmt.Scanf/fmt.Scanln

    这三个方法接收n个指针类型的参数(包括string 指针)将os.Stdin 输入内容传入 参数变量中,但三个方法之间有一定的区别:

    • fmt.Scan 接收参数,会识别空格左右的内容,哪怕换行符号存在也不会影响scan对内容的获取
    • fmt.Scanf 接收格式化输入参数,会对参数值进行格式化判断,格式化类似 Printf(标准化输出的时候详细描述,除了%p %T等少量格式未实现)
    • fmt.Scanln 接收参数,不识别空格左右的内容,一旦遇到换行符就结束输入

    fmt.Scan示例:

    	var (
    		a, b, c int
    	)
    	fmt.Scan(&a, &b, &c)
    	fmt.Println(a, b, c)
    

    编译运行:

    输入: 
    33 55 回车
    11
    输出:33 55 11
    

    fmt.Scanf 示例:

    	var (
    		a, b, c int
    	)
    	fmt.Scanf("%d %d %d", &a, &b, &c)
    	fmt.Println(a, b, c)
    

    编译运行:

    输入:1 2 3
    输出:1 2 3
    

    fmt.Scanln示例:

    	var (
    		a, b, c int
    	)
    	fmt.Scanln(&a, &b, &c)
    	fmt.Println(a, b, c)
    

    编译运行:

    输入:1 2 回车
    输出: 1 2 0
    
  • fmt.Sscan/fmt.Sscanf/fmt.Sscanln

    三个方法是从string 中读取值,用将string取代 os.Stdin 输入;功能区别与 fmt.Scan、fmt.Scanf 和 fmt.Scanln基本一致,只是获取值的输入方式不一致,基本示例:

    	var a int
    	var b int
    	var s string
    	inS := "1 2 hello"
    	fmt.Sscan(inS, &a, &b, &s) //fmt.Sscan操作示例
    	fmt.Println(a, b, s)
    	fmt.Sscanf(inS, "%d %d %s", &a, &b, &s) //fmt.Sscanf操作示例
    	fmt.Println(a, b, s)
    	fmt.Sscanln(inS, &a, &b, &s) //fmt.Sscanln操作示例
    	fmt.Println(a, b, s)
    
  • fmt.Fscan/fmt.Fscanf/fmt.Fscanln

    这三个方法则是通过在提供的实现了io.Reader 接口的Read方法的输入,fmt.Scan、fmt.Scanf 和 fmt.Scanln函数的底层掉用最终也是掉用这三个函数,也是这三个函数的封装,

    底层fmt.Fscan/fmt.Fscanf/fmt.Fscanln源码如下:

    func Scan(a ...interface{}) (n int, err error) {
    	return Fscan(os.Stdin, a...)
    }
    func Scanln(a ...interface{}) (n int, err error) {
    	return Fscanln(os.Stdin, a...)
    }
    func Scanf(format string, a ...interface{}) (n int, err error) {
    	return Fscanf(os.Stdin, format, a...)
    }
    

    mt.Fscan/fmt.Fscanf/fmt.Fscanln 调用示例:

    	var i int
    	fmt.Fscan(os.Stdin,&i)
    	fmt.Fscanf(os.Stdin,"%d",&i)
    	fmt.Fscanln(os.Stdin,&i)
    	fmt.Println(i)
    
  • bufio.Reader

    通过读取标准Stdin 输入进行输入, 但它可以设置部分读取后部分处理然后再处理后面的内容,bufio.Reader提供了函数处理输入:

    func (b *Reader) Read(p []byte) (n int, err error) 
    func (b *Reader) ReadByte() (byte, error) 
    func (b *Reader) ReadRune() (r rune, size int, err error) 
    func (b *Reader) ReadSlice(delim byte) (line []byte, err error) 
    func (b *Reader) ReadLine() (line []byte, isPrefix bool, err error) 
    func (b *Reader) ReadBytes(delim byte) ([]byte, error) 
    func (b *Reader) ReadString(delim byte) (string, error) 
    .......
    

    代码示例:

    	in := bufio.NewReader(os.Stdin)
    	ret,err := in.ReadString('\n')
    	fmt.Println(ret, err)
    

    编译运行:

    输入: 11  回车
    输出: 11
     <nil>
    

# 2.4.2 标准输出

fmt.Print 输出到控制台

fmt.Println 输出到控制台并换行

fmt.Printf 输出格式化的字符串

fmt.Sprintf 格式化并返回一个字符串而不带任何输出

fmt.Fprintf 格式化并输出到 io.Writers

代码演示:

	var s string = "hello"
	fmt.Print(s) //输出s字符串
	fmt.Println(s) //输出s字符串并换行
	fmt.Printf("%s\n", s) //格式化输出字符串s
	s1 := fmt.Sprintf("%s", s) //返回s字符串并不直接输出
	fmt.Println(s1)
	fmt.Fprintf(os.Stdout, "%s World !\n", s) //使用 Fprintf 来格式化并输出到 io.Writers

编译运行:

hellohello
hello
hello
hello World !

我们都知道 fmt.Printf 与 fmt.Sprintf fmt.Fprintf 等格式化函数都需要格式化参数,那格式化参数有哪些呢,我这边列出表格如下:

参数 描述
%v 值的默认格式
%+v 返回带字段格式的值
%#v 值的Go语法表示
%T 值的类型的Go语法表示
%% 一个%字面量,并非值的占位符
%t 以true或者false输出的布尔值
%b 二进制表示
%c 相应Unicode码点所表示的字符
%d 十进制表示
%e 以科学记数法e表示的浮点数或者复数值,例如 -1234.456e+78
%E 以科学记数法e表示的浮点数或者复数值,例如 -1234.456E+78
%f 以标准记数法表示的浮点数或者复数值
%g 以%e或者%f表示的浮点数或者复数,任何一个都以最为紧凑的方式输出
%G 以%E或者%f表示的浮点数或者复数,任何一个都以最为紧凑的方式输出
%o 一个以八进制表示的数字(基数为8)
%p 指针地址输出,十六进制表示,前缀 0x
%q 双引号围绕的字符串,由Go语法安全地转义
%s 字符串
%U 一个用Unicode表示法表示的整型码点,默认值为4个数字字符
%x 十六进制,小写字母,每字节两个字符
%X 十六进制,大写字母,每字节两个字符

# 2.5 数据类型转换

# 2.5.1 类型转换

  • 强制转换

    强制转换的语法格式为:

    var a T = (T)(b)

    其中 T 为 强制转换的类型, a,b分别为变量;那什么情况下适合强制类型转换呢,只有相同底层类型的变量之间可以进行相互转换, 不同底层类型的变量相互转换时会引发编译错误

    同底层类型正确强制转换示例:

    	a := 1.1 //定义a为float类型,值为1.1
    	b := int(a) //强制转换a为int,赋值给b
    	fmt.Printf("%T, %v \n", b, b)
    
    	c := 1 //定义c为int类型,值为1
    	f := float64(c) //强制转换c为float64,赋值给f
    	fmt.Printf("%T, %v \n", f, f)
    
    	var d float32 = 1.00 //定义c为float32类型,值为1.00
    	e := float64(d) //强制转换c为float64,赋值给e
    	fmt.Printf("%T, %v \n", e, e)
    
    	s := "hello" //定义s为string类型,值为hello
    	s1 := []byte(s) //强制转换s为[]byte,赋值给s1
    	fmt.Printf("%T, %v \n", s1, s1)
    

    编译执行:

    int, 1
    float64, 1
    float64, 1
    []uint8, [104 101 108 108 111]
    

    不同底层类型转换示例:

    	a := 1.1 //定义a为float类型,值为1.1
    	f := bool(a)
    	fmt.Printf("%T, %v", f, f)
    

    编译执行报错:

    .\package.go:15:11: cannot convert a (type float64) to type bool
    

    另外,需要注意的是:当从一个取值范围较大的类型转换到取值范围较小的类型时(将 int32 转换为 int16 或将 float32 转换为 int),会发生精度丢失(截断)的情况

  • string 与 int转换

    • int->string

      方法一: 采取 Sprintf 格式化输出

      示例:

      	a := 22
      	s := fmt.Sprintf("%v", a)
      	fmt.Printf("%T, %v", s, s)
      

      编译运行:

      string, 22
      

      方法二: 使用 strconv 包 Itoa 函数

      示例:

      	a := 22
      	s := strconv.Itoa(a) //int转string,参数只接受int类型,不接收int32 int64
      	fmt.Printf("%T, %v", s, s)
      
      	var a1 int32 = 223
      	s1 := strconv.FormatInt(int64(a1), 10) //int32转string,参数只接受int64,需要将int32转为int64后操作
      	fmt.Printf("%T, %v", s1, s1)
      
      	var a2 int64 = 222
      	s2 := strconv.FormatInt(a2, 10) //int64转string,参数只接受int64
      	fmt.Printf("%T, %v", s2, s2)
      

      编译运行:

      string, 22
      string, 223
      string, 222
      
    • string -> int

      方法: 使用strocnv包中的转换函数

      示例:

      	var i string = "21333"
      	s, _ := strconv.Atoi(i) //string转为int类型
      	fmt.Printf("%T, %v\n", s, s)
      
      	s1, err := strconv.ParseInt(i, 10, 32)  //string转为int32类型
      	if err == nil{
      		j := int32(s1)
      		fmt.Printf("%T, %v\n", j, j)
      	}
      	
      	s2, _ := strconv.ParseInt(i, 10, 64)  //string转为int64类型
      	fmt.Printf("%T, %v\n", s2, s2)
      

      编译输出:

      int, 21333
      int32, 21333
      int64, 21333
      
  • string 与 布尔类型

    方法: 使用 strocnv包中的转换函数

    示例:

    	s := "hello"
    	b, _ := strconv.ParseBool(s) //string转bool类型
    	fmt.Printf("%T, %v \n", b, b)
    
    	var b1 bool = true
    	s1 := strconv.FormatBool(b1) //bool转string类型
    	fmt.Printf("%T, %v", s1, s1)
    

    编译输出:

    bool, false
    string, true
    
  • string与float类型

    方法: 使用 strocnv包中的转换函数

    示例:

    	s := "27.7512"
    	f, err := strconv.ParseFloat(s, 32) //string转float32 类型,返回值为float64,转为float32需要强转一次
    	if err == nil{
    		f1 := float32(f)
    		fmt.Printf("%T, %v \n", f1, f1)
    	}
    
    	f2, _ := strconv.ParseFloat(s, 64) //string转float64 类型
    	fmt.Printf("%T, %v \n", f2, f2)
    
    	var f3 float32 = 2.8888
    	f3_1 := float64(f3)
    	s3 := strconv.FormatFloat(f3_1, 'f', 3, 64) //float64转string 类型,只接受float64参数,float32需要强转为float64后操作
    	fmt.Printf("%T, %v \n", s3, s3)
    

    编译输出:

    float32, 27.7512
    float64, 27.7512
    string, 2.889
    
  • int与float类型

    方法: 可以使用强行转换方式

    示例:

    	f := 188.5848
    	i := int(f) //float转int
    	i2 := int64(f) //float转int64
    	i3 := int32(f) //float转int32
    	fmt.Printf("%T,%v\n", i , i)
    	fmt.Printf("%T,%v\n", i2 , i2)
    	fmt.Printf("%T,%v\n", i3 , i3)
    
    	i4 := 1
    	f1 := float64(i4) //int转float64
    	f2 := float32(i4) //int转float32
    	fmt.Printf("%T,%v\n", f1 , f1)
    	fmt.Printf("%T,%v\n", f2 , f2)
    

    编译输出:

    int,188
    int64,188
    int32,188
    float64,1
    float32,1
    
  • 指定类型转换成字符串后追加到切片

    Append 系列函数用于将指定类型转换成字符串后追加到一个切片中,其中包含 AppendBool()、AppendFloat()、AppendInt()、AppendUint()。

    Append 系列函数和 Format 系列函数的使用方法类似,只不过是将转换后的结果追加到一个切片中。

       b10 := []byte("int (base 10):")
     
        // 将转换为10进制的string,追加到slice中
        b10 = strconv.AppendInt(b10, -42, 10)
        fmt.Println(string(b10))
        b16 := []byte("int (base 16):")
        b16 = strconv.AppendInt(b16, -42, 16)
        fmt.Println(string(b16))
    

    编译输出:

    int (base 10):-42
    int (base 16):-2a
    
  • 指针类型转换

    直接来看一个例子:

    	var i int = 1
    	var p *int = &i //定义指针p为int型
    	var c *int64 = (*int64)(p) //强制转换p指针为int64指针
    	fmt.Println(*c)
    

    上面代码想通过指针直接强转指针,将p的int指针转为int64指针,最后编译报错: cannot convert p (type *int) to type *int64

    我们可以通过 unsafe.Pointer 来执行这种操作,修改代码如下:

    	var i int = 1
    	var p *int = &i //定义指针p为int型
    	var c *int64 = (*int64)(unsafe.Pointer(p)) //强制转换p指针为int64指针
    	fmt.Printf("%T, %v", *c, *c)
    

    编译结果: int64, 1

  • 类型断言

    类型断言(type assertion) 是将接口类型的值x,转换成类型T,类型断言的必要条件是x是接口类型,非接口类型的x不能做类型断言,基本格式为:

    v := x.(T)
    v, ok := x.(T)
    

    接口类型断言代码示例:

    	var x interface{} = 7
    	v := x.(int) //断言x为int值,返回v值为x值
    	fmt.Println(v)
    	v, ok := x.(int) //断言x为int值,返回变量v值为x值,变量ok为是否断言正确
    	fmt.Println(v, ok)
    

    编译运行:

    7
    7 true
    

    需要注意的是,如果采取 v := x.(T) 方式断言,断言失败,系统会抛出异常,示例如下:

    	var x interface{} = 7
    	v := x.(string) //断言x为string值
    	fmt.Println(v)
    

    编译运行:

    panic: interface conversion: interface {} is int, not string
    

    综上所述,为了确保系统正常稳定的执行,一般推荐 v, ok := x.(T) 模式,然后对ok状态进行判断。

    断言还有一个用途就是利用 variable.(type) 配合switch进行类型判断,代码示例如下:

    	var x interface{} = 7
    	switch x.(type) {
    	case int:
    		fmt.Println("int")
    	case string:
    		fmt.Println("string")
    	case float64:
    		fmt.Println("float64")
    	default:
    		fmt.Println("unknow")
    	}
    

    上述代码 是 利用 x.(type) 的类型值,配合 switch 进行一系列类型判断。

# 2.5.2 float 精度丢失问题

几乎所有的编程语言都有精度丢失这个问题,这是典型的二进制浮点数精度损失问题,在定长条件下,二进制小数和十进制小数互转可能有精度丢失。具体来看个例子:

	num := 19.9
	last := num * 100
	fmt.Println(last)

编译运行:

1989.9999999999998

按我们直观运算, 19.9 * 100 = 1990, 但程序执行得到的结果是: 1989.9999999999998 , 这样就造成了精度丢失,这个在金融行业以及对计算金额精准度高要求的行业来说是不可接受的,那我们为了解决这个问题,就引入了 decimal 包来解决,代码如下:

import (
	"fmt"
	"github.com/shopspring/decimal"
)

func main() {
	num := 19.9
	decimalValue := decimal.NewFromFloat(num)
	decimalValue = decimalValue.Mul(decimal.NewFromInt(100)) 
	res,_ := decimalValue.Float64()
	fmt.Println(res)
}

编译运行:

1990

decimal 包提供了很多运算方法,比如: decimal.NewFromInt decimal.NewFromInt32 decimal.NewFromBigInt NewFromString NewFromFloat NewFromFloat32 等函数来接受处理输入的参数,也提供了 decimal.Mul(乘法) decimal.Sub(减法) decimal.Add(加法) decimal.Div(除法) 等运算方法来帮助解决运算精度问题。

# 2.6 数组与切片

# 2.6.1 数组 (Array)

数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。在Go中比较少直接使用数组。

数组是值类型,赋值和传参会复制整个数组,而不是指针。因此改变副本的值,不会改变本身的值。

数组声明语法如下:

var 数组变量名 [元素数量]Type

语法说明如下所示:

  • 数组变量名:数组声明及使用时的变量名。
  • 元素数量:数组的元素数量,可以是一个表达式,但最终通过编译期计算的结果必须是整型数值,元素数量不能含有到运行时才能确认大小的数值。
  • Type:可以是任意基本类型,包括数组本身,类型为数组本身时,可以实现多维数组。

一维数组声明以及赋值示例:

	var a [2]int   //声明一个长度为2的int型数组变量a
	a[0] = 1       //赋值索引0值为1
	a[1] = 2       //赋值索引1值为2
	fmt.Println(a) //打印输出数组a

	b := [3]int{1, 2, 3} //声明一个长度为3的int型数组变量b,并且初始化值为 1,2,3
	fmt.Println(b)       //打印输出数组b

	var c [2]string = [2]string{"hello", "world"} //声明一个长度为2的string型数组变量c,并且初始化值为 "hello", "world"
	fmt.Println(c)                                //打印输出数组c

	var d [2]float64         //声明一个长度为2的float64型数组变量d,
	d = [2]float64{1.2, 2.3} //初始化值为 1.2, 2.3
	fmt.Println(d)           //打印输出数组d

上面代码展示了几种声明与赋值数组的方法,需要知道数组长度可以用内置函数 len() 来获取。

在数组的定义中,如果在数组长度的位置出现“...”省略号,则表示数组的长度是根据初始化值的个数来计算,因此,上面数组 b 的定义可以简化为:

	b := [...]int{1,2,3}
	fmt.Println(b)

比较两个数组是否一致,我们可以直接比较运算符 (==!=)来判断两个数组是否相等, 只有当两个数组的所有元素都是相等的时候数组才是相等的,不能比较两个类型不同的数组,否则程序将无法完成编译。示例如下:

	a := [...]int{1, 2, 3, 4, 5, 6}
	b := [6]int{1, 2, 3, 4, 5, 6}
	c := [2]int{2, 4}
	d := [2]string{"2", "4"}
	
	if a == b {
		fmt.Println("a = b") //正常输出a=b
	} else {
		fmt.Println("a != b")
	}

	/** 下面代码编译会编译错误 */
	if b == c { //b和c个数不一样,不能进行比较
		fmt.Println("c = b")
	}else{
		fmt.Println("c != b")
	}
	if c == d {  //d和c个类型不一样,不能进行比较
		fmt.Println("c = d")
	}else{
		fmt.Println("c != d")
	}

多维数组声明以及赋值示例:

	a := [2][2]int{{1, 2}, {2, 3}} //声明并初始化二维数组a
	fmt.Println(a)

	var b [1][2]string //声明二维数组b
	b[0][0] = "hello"  //赋值二维数组b[0][0]
	b[0][1] = "world"  //赋值二维数组b[0][1]
	fmt.Println(b)

	var c [2][3]int = [2][3]int{{1, 2, 3}, {2, 3, 4}} //声明并初始化二维数组c
	fmt.Println(c)

数组遍历可以使用 range 方法,也可以使用 for 循环,示例如下:

	a := [3]int{1, 2, 3}

	//使用 range 遍历数组
	for k, v := range a {
		fmt.Printf("k:%d v:%d \n", k, v)
	}

	//使用for循环根据长度来遍历数组
	for i:=0; i<len(a); i++{
		fmt.Printf("k:%d v:%d \n", i, a[i])
	}

# 2.6.2 切片 (Slice)

切片是对数组的一个连续片段的引用,所以切片是一个引用类型,切片可以看作一个可变长的数组。

切片的内部结构包含地址、大小和容量。

# 2.6.2.1 切片创建与初始化

  • 从数组或者切片生成新切片

    从数组或者切片生成切片是常见的操作,格式如下:

    slice [开始位置 : 结束位置]

    语法说明如下:

    • slice:表示目标数组或者切片对象;
    • 开始位置:对应目标数组或切片对象的索引;
    • 结束位置:对应目标数组或切片的结束索引。

    代码示例:

    	a := [5]int{1, 2, 3, 4, 5} //定义并初始化数组
    	b := a[1:3] //取a数组索引1到3索引位置值作为切片
    	fmt.Println(b)
    

    编译执行:

    [2 3]
    

    根据索引位置取切片 slice 元素值时,取值范围是(0~len(slice)-1),超界会报运行时错误,生成切片时,结束位置可以填写 len(slice) 但不会报错。

    切片或者数组取元素还有更多玩法,参考下表:

    操作 说明
    s[n] 切片s中索引位置为n得值
    s[:] 从切片s索引位置 0 到 len(s)-1处所获取切片
    s[low:] 从切片s索引位置 low 到 len(s)-1处所获取切片
    s[:high] 从切片s索引位置 0到 high处所获取切片
    s[low:high] 从切片s索引位置 low 到 high处所获取切片
    s[low:high:max] 从切片s索引位置 low 到 high处所获取切片; len = hight-low ,cap=max-low
    s[0:0] 清空切片

    切片或者数组取元素示例:

    	a := [8]int{1, 2, 3, 4, 5, 7, 8, 0} //定义并初始化数组
    	b := a[1:3]                      //取a数组索引1到3索引位置值作为切片
    	c := a[:]                        //从切片s索引位置 0 到 len(a)-1处所获取切片
    	d := a[1:]                       //从切片s索引位置 1 到 len(a)-1处所获取切片
    	e := a[:6]                       //从切片s索引位置 0 到 6处所获取切片
    	fmt.Println(a, b, c, d, e)
    

    编译执行:

    [1 2 3 4 5 7 8 0] [2 3] [1 2 3 4 5 7 8 0] [2 3 4 5 7 8 0] [1 2 3 4 5 7]
    
  • 直接声明切片

    直接声明切片类型声明格式如下:

    var name []Type

    其中 name 表示切片的变量名,Type 表示切片对应的元素类型。这个跟数组声明很像,只是切片是可变长的,不需要提前设定长度。

    直接声明切片类型以及初始化示例:

    	var a []int        //声明切片a
    	a = []int{1, 2, 3} //初始化赋值切片a
    
    	b := []string{"hello", "hello"} //声明并直接初始化切片b
    
    	var c []int = []int{2, 3, 4} //声明并直接初始化切片c
    
  • make()函数构造切片

    如果需要动态地创建一个切片,可以使用 make() 内建函数,格式如下:

    make( []Type, size, cap )

    其中 Type 是指切片的元素类型,size 指的是为这个类型分配多少个元素,cap 为预分配的元素数量,这个值设定后不影响 size,只是能提前分配空间,降低多次分配空间造成的性能问题。操作示例:

    	a := make([]int, 2, 4)
    	a[0] = 5
    	b := make([]int, 3)
    	fmt.Println(a, len(a), cap(a))
    	fmt.Println(b, len(b), cap(b))
    

    运行编译:

    [5 0] 2 4
    [0 0 0] 3 3
    

    从代码中可以看出make的第2个参数为切片长度, 第3个参数为切片的容量,容量的查看可以使用 cap()函数来查看

  • 多维切片

    Go语言中同样允许使用多维切片,声明一个多维数组的语法格式如下:

    var sliceName [][]...[]sliceType

    其中,sliceName 为切片的名字,sliceType为切片的类型,每个[ ]代表着一个维度,切片有几个维度就需要几个[ ]

    下面以二维切片为例,声明一个二维切片并赋值,代码如下:

    //声明一个二维切片
    var slice [][]int
    //为二维切片赋值
    slice = [][]int{{10}, {100, 200}}
    

# 2.6.2.2 切片追加元素/复制元素/删除元素

  • 切片元素追加

    切片的追加元素可以使用内置函数 append() 来操作,代码演示如下:

    	var a []int
    	var b []int = []int{7, 8}
    	a = append(a, 1) //追加一个参数
    	a = append(a, 2, 3, 4) //追加多个参数
    	a = append(a, []int{5, 6}...) //追加切片
    	a = append(a, b...)  //追加切片
    	fmt.Println(a)
    

    上述代码列举了 追加一个或者多个以及追加切片的操作方式,不过需要注意的是,在使用 append() 函数为切片动态添加元素时,如果空间不足以容纳足够多的元素,切片就会进行“扩容”,此时新切片的长度会发生改变。具体扩容以及数组和切片更底层的说明将后续详细说明。

  • 切片复制

    切片的复制可以使用内置函数 copy() 来操作,函数使用的格式如下:

    copy( destSlice, srcSlice []T) int

    其中 srcSlice 为数据来源切片,destSlice 为复制的目标(也就是将 srcSlice 复制到 destSlice),目标切片必须分配过空间且足够承载复制的元素个数,并且来源和目标的类型必须一致,copy() 函数的返回值表示实际发生复制的元素个数。

    下面代码将演示 copy()函数的使用:

    	s1 := []int{1, 2, 3}
    	s2 := []int{11, 12, 13, 14}
    	copy(s1, s2) //将s2复制到s1,由于s1只有三个元素,所以只能复制s2的前三个元素到s1中
    	fmt.Println(s1, s2)
    	copy(s2, s1) //将s1复制到s2
    	fmt.Println(s1)
    

    使用 copy()最大的好处修改源切片值不会对复制切片不产生影响,如果采取直接引用的方案,源切片改动则会影响后续切片数据。下面示例通过代码演示对切片的引用和复制操作后对切片元素的影响:

    	// 设置元素数量为1000
    	const elementCount = 100
    	// 预分配足够多的元素切片
    	srcData := make([]int, elementCount)
    	// 将切片赋值
    	for i := 0; i < elementCount; i++ {
    		srcData[i] = i
    	}
    	// 引用切片数据
    	refData := srcData
    	// 预分配足够多的元素切片
    	copyData := make([]int, elementCount)
    	// 将数据复制到新的切片空间中
    	copy(copyData, srcData)
    	// 修改原始数据的第一个元素
    	srcData[0] = 999
    	// 打印引用切片的第一个元素
    	fmt.Println(refData[0])
    	// 打印复制切片的第一个和最后一个元素
    	fmt.Println(copyData[0], copyData[elementCount-1])
    	// 复制原始数据从4到6(不包含)
    	copy(copyData, srcData[4:6])
    	for i := 0; i < 5; i++ {
    		fmt.Printf("%d ", copyData[i])
    	}
    

    编译运行:

    999
    0 999
    4 5 2 3 4
    
  • 切片元素删除

    Go语言并没有对删除切片元素提供专用的语法或者接口,需要使用切片本身的特性来删除元素,我们可以采取 从数组和切片中生成新切片那篇幅中的操作手法来操作删除元素,代码示例:

    	a := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    	a = a[1:]        //删除第一个元素
    	fmt.Println(a)
    	a = a[:len(a)-1] //删除最后一个元素
    	fmt.Println(a)
    	index := 5
    	a = append(a[:index-1], a[index+1:]...) //删除指定索引元素
    	fmt.Println(a)
    	a = a[0:0] // 清空切片
    

    编译运行结果:

    [2 3 4 5 6 7 8 9 10]
    [2 3 4 5 6 7 8 9]
    [2 3 4 5 8 9]
    

# 2.6.2.3 值类型与引用类型

在数组篇幅中,我们讲到数组是值类型, 切片篇幅中也提到,切片是引用类型,那什么是值类型和引用类型呢?

值类型:类型的变量的值存储在栈中,修改值, 不会对源产生影响,值类型分别有: int系列、float系列、bool、string、数组和结构体等。

引用类型: 变量存储的是一个地址,这个地址对应的空间才真正存储数据值,内存通常在堆上分配,当没有任何变量引用这个地址时,该地址对应的数据空间就成为一个垃圾,由GC来回收。改变任何一个变量值,相当于改了自己变量的值。引用类型分别有: 指针、slice切片、管道channel、接口interface、map、函数等。

下面来代码展示值类型和引用类型的区别,我们分别用int系列和切片做示例:

	x := 1
	y := x
	y = 2
	fmt.Printf("x = %d; y= %d \n", x, y)

	s := []int{1, 2, 3}
	s1 := s
	s1[0] = 12
	fmt.Println(s, s1)

编译运行:

x = 1; y= 2
[12 2 3] [12 2 3]

从上述代码中, 将 x 值赋予 y ,在改变 y值后, x 值不变,这种就说值引用的特点, 而 将切片 s 赋值给 s1, 更改s1值,直接影响s,这种改变变量值等于改变自己的值的操作就是引用类型特点。

# 2.6.2.4 nil切片 、空切片 、零切片

讲一个非常细节的小知识,这个知识被大多数 Go 语言的开发者无视了,它就是切片的三种特殊状态 —— 「零切片」、「空切片」和「nil 切片」

切片被视为 Go 语言中最为重要的基础数据结构,使用起来非常简单,有趣的内部结构让它成了 Go 语言面试中最为常见的考点。切片的底层是一个数组,切片的表层是一个包含三个变量的结构体,当我们将一个切片赋值给另一个切片时,本质上是对切片表层结构体的浅拷贝。结构体中第一个变量是一个指针,指向底层的数组,另外两个变量分别是切片的长度和容量。

切片结构体定义:

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

结构示例图:

特殊状态之一「零切片」其实并不是什么特殊的切片,它只是表示底层数组的二进制内容都是零。比如下面代码中的 s 变量就是一个「零切片」, 看如下代码:

	var s = make([]int, 5)
	fmt.Println(s)
	var s1 = make([]*int, 5)
	fmt.Println(s1)

输出结果为:

[0 0 0 0 0]
[<nil> <nil> <nil> <nil> <nil>]

[ 零切片」其实就是底层数组值全为0, 指针类型得话,值全部为 nil得切片而已,比较容易理解,就不多累赘了。

下面我们要引入「空切片」和 「nil 切片」,在理解它们的区别之前我们先看看一个长度为零的切片都有那些形式可以创建出来,代码如下:

	var s1 []int
	var s2 = make([]int, 0)
	var s3 []int = []int{}
	var s4 []int = *new([]int)
	fmt.Println(len(s1), len(s2), len(s3), len(s4))
	fmt.Println(cap(s1), cap(s2), cap(s3), cap(s4))
	fmt.Println(s1, s2, s3, s4)

结果为:

0 0 0 0
0 0 0 0
[] [] [] []

面这四种形式从输出结果上来看,似乎一摸一样,没区别。但是实际上是有区别的,我们要讲的两种特殊类型「空切片」和「 nil 切片」,就隐藏在上面的四种形式之中。 我们如何来分析三面四种形式的内部结构的区别呢?接下里要使用到 Go 语言的高级内容,通过 unsafe.Pointer 来转换 Go 语言的任意变量类型。 因为切片的内部结构是一个结构体,包含三个机器字大小的整型变量,其中第一个变量是一个指针变量,指针变量里面存储的也是一个整型值,只不过这个值是另一个变量的内存地址。我们可以将这个结构体看成长度为 3 的整型数组 [3]int。然后将切片变量转换成 [3]int。

	var s1 []int
	var s2 = make([]int, 0)
	var s3 []int = []int{}
	var s4 []int = *new([]int)

	var a1 = *(*[3]int)(unsafe.Pointer(&s1))
	var a2 = *(*[3]int)(unsafe.Pointer(&s2))
	var a3 = *(*[3]int)(unsafe.Pointer(&s3))
	var a4 = *(*[3]int)(unsafe.Pointer(&s4))

	fmt.Println(a1, a2, a3, a4)

结果:

[0 0 0] [824634170872 0 0] [824634170872 0 0] [0 0 0]

从输出中我们看到了明显的神奇的让人感到意外的难以理解的不一样的结果。 其中输出为 [0 0 0] 的 s1 和 s4 变量就是「 nil 切片」,s2 和 s3 变量就是「空切片」。824634170872 这个值是一个特殊的内存地址,所有类型的「空切片」都共享这一个内存地址。

用图形来表示「空切片」和「 nil 切片」如下:

空切片指向的 zerobase 内存地址是一个神奇的地址。

最后一个问题是:「 nil 切片」和 「空切片」在使用上有什么区别么? 答案是完全没有任何区别!No!不对,还有一个小小的区别!请看下面的代码:

	var s1 []int
	var s3 []int = []int{}
	fmt.Println(s1 == nil)
	fmt.Println(s3 == nil)
	fmt.Printf("%#v\n", s1)
	fmt.Printf("%#v\n", s3)

运行结果:

true
false
[]int(nil)
[]int{}

所以为了避免写代码的时候把脑袋搞昏的最好办法是不要创建「 空切片」,统一使用「 nil 切片」,同时要避免将切片和 nil 进行比较来执行某些逻辑。这是官方的标准建议。

The former declares a nil slice value, while the latter is non-nil but zero-length. They are functionally equivalent—their len and cap are both zero—but the nil slice is the preferred style.

[空切片」和「 nil 切片」还有一个极为不同的地方在于 JSON 序列化,代码如下:

	var s1 = TestData{}
	var s3  = TestData{[]int{}}
	j1, _ := json.Marshal(s1)
	j2, _ := json.Marshal(s3)
	fmt.Println(string(j1))
	fmt.Println(string(j2))

它们的 json 序列化结果居然也不一样, 看结果:

{"Values":null}
{"Values":[]}

还有最后一点非常重要,不管空切片 还是 nil切片都是不能直接赋值使用,如下代码:

	var s1 []int
	s1[1] = 0
	fmt.Println(s1)

结果:

panic: runtime error: index out of range

示例2:

	var s2 []int = []int{}
	s2[1] = 0
	fmt.Println(s2)

结果:

panic: runtime error: index out of range

综合上面所述,空切片和nil切片,都未申请空间,强行赋值为报错,解决这个问题就需要使用 make函数或者初始化一个值去自动申请空间。

# 2.7 Map

# 2.7.1 Map声明和初始化

Go语言中 map 是一种特殊的[数据结构],一种元素对(pair)的无序集合,pair 对应一个 key(索引)和一个 value(值),所以这个结构也称为关联数组或字典,这是一种能够快速寻找值的理想结构,给定 key,就可以迅速找到对应的 value。

map 这种数据结构在其他编程语言中也称为字典、hash 和 HashTable 等。

map 是引用类型,未初始化的 map 的值是 nil,声明语法:

var mapName map[keyType]valueType

说明 :

  • mapName 为 变量名
  • keyType 为键值类型
  • valueType 为值类型

除了使用此声明方法外,也可以使用make来声明,语法如下:

make(map[keyType]valueType, cap)

其中:

  • keyType 为 键值类型
  • valueType 为值类型
  • cap为 map 的初始容量 capacity,当然这个参数也可以为空

【示例】

	/** map 声明初始化方式一 */
	var m map[int]string                       //声明变量m为map类型,其中 key为int型,值为string型
	m = map[int]string{0: "hello", 1: "world"} //给变量m赋值
	fmt.Println(m)

	/** map 声明初始化方式二 */
	var m1 map[int]string = map[int]string{0: "hello", 1: "world"} //声明变量m1为map类型,其中 key为int型,值为string型并且赋值
	fmt.Println(m1)

	/** map 声明初始化方式三 */
	m2 := map[int]string{0: "hello", 1: "world"} //声明变量m2为map类型,其中 key为int型,值为string型并且赋值
	fmt.Println(m2)

	/** map 声明初始化方式四 */
	m3 := make(map[int]string)          //声明变量m2为map类型,其中 key为int型,值为string型
	m3 = map[int]string{0: "你", 1: "好"} //给变量m3赋值
	fmt.Println(m3)

除了常规的类型作为map的键和值以外,切片也可以作为map的值,示例如下:

	var mapSlice map[int][]string = map[int][]string{0:{"hello", "world"}, 1:{"hello", "ok"}}
	fmt.Println(mapSlice)

# 2.7.2 Map遍历

map 的遍历过程基本跟数组和切片一致,使用 for range 循环完成,示例:

	var m map[int]int = map[int]int{0: 1, 1: 2, 2: 3, 3: 4}
	m[4] = 5
	for k, v := range m {
		fmt.Println(k, v)
	}

对于Go语言的很多对象来说都是差不多的,直接使用 for range 语法即可,遍历时,可以同时获得键和值,如只遍历值,可以使用下面的形式:

for _, v := range m {

只遍历键时,使用下面的形式:

for k := range m {

另外,遍历输出元素的顺序与填充顺序无关,不能期望 map 在遍历时返回某种期望顺序的结果,如果需要特定顺序的遍历结果,正确的做法是先排序,代码如下:

	var m map[int]int = map[int]int{0: 5, 1: 1, 2: 6, 3: 4, 4: 8}
	var sceneList []int
	for _, v := range m {
		sceneList = append(sceneList, v) //将值单独存储到切片
	}
	sort.Ints(sceneList) //对切片排序
	fmt.Println(sceneList)

编译运行:

[1 4 5 6 8]

# 2.7.3 Map元素删除和清空

查看一个键值是否存在Map,数组,切片中可以下面方式:

    value, ok := map[key]

而删除单个键值对,可以使用内置函数 delete() 来进行操作,示例:

	scoreMap := make(map[string]int)
	scoreMap["小华"] = 85
	scoreMap["小徐"] = 100
	scoreMap["小郑"] = 60
	if _, ok := scoreMap["小徐"]; ok {
		delete(scoreMap, "小徐") //使用delete删除元素
	}
	fmt.Println(scoreMap)

编译运行:

map[小华:85 小郑:60]

Go语言中并没有为 map 提供任何清空所有元素的函数、方法,清空 map 的唯一办法就是重新 make 一个新的 map,不用担心垃圾回收的效率,Go语言中的并行垃圾回收效率比写一个清空函数要高效的多。

# 2.8 指针

Go语言为程序员提供了控制数据结构指针的能力,但是,并不能进行指针运算, 是安全指针。

一个指针变量可以指向任何一个值的内存地址,它所指向的值的内存地址在 32 和 64 位机器上分别占用 4 或 8 个字节,占用字节的大小与所指向的值的大小无关。当一个指针被定义后没有分配到任何变量时,它的默认值为 nil。指针变量通常缩写为 ptr。

每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go语言中使用在变量名前面添加&操作符(前缀)来获取变量的内存地址(取地址操作),格式如下:

var ptr *type= &v // v 的类型为 T

其中type代表变量类型, *type代表变量指针类型, v 代表被取地址的变量,变量 v 的地址使用变量 ptr 进行接收,ptr 的类型为*T,称做 T 的指针类型,*代表指针。

【示例】

	var s string = "hello"
	var i int = 1
	var sp *string = &s //定义指针并且赋值
	fmt.Printf("%p %p", sp, &i)

编译运行:

0xc0000421c0 0xc000056058

指针的值是带有0x十六进制前缀的一组数据,变量、指针和地址三者的关系是,每个变量都拥有地址,指针的值就是地址。

当使用&操作符对普通变量进行取地址操作并得到变量的指针后,可以对指针使用*操作符,也就是指针取值,也可以修改值。示例如下:

	s := "Go 语言中指针是很容易学习的"
	ptr := &s //定义ptr指针,取s地址赋值给ptr指针
	fmt.Printf("ptr type:%T, address:%p\n", ptr, ptr)
	v := *ptr //取值赋值给v
	fmt.Printf("v type:%T, value:%s\n", v, v)
	*ptr = "指针是很容易学习的" //通过指针修改值
	fmt.Printf("s type:%T, value:%s\n", s, s)

编译运行:

ptr type:*string, address:0xc0000421c0
v type:string, value:Go 语言中指针是很容易学习的
s type:string, value:指针是很容易学习的

Go语言还提供了另外一种方法来创建指针变量,格式如下:new(类型),示例如下:

	str := new(string)
	*str = "Go语言"
	fmt.Println(*str)

Go语言中 new 和 make 是两个内置函数,主要用来创建并分配类型的内存。在我们定义变量的时候,可能会觉得有点迷惑,不知道应该使用哪个函数来声明变量,其实他们的规则很简单,new 只分配内存,而 make 只能用于 slice、map 和 channel 的初始化,详细的分解会在深入的章节详细介绍。

最后更新时间: 3/2/2023, 4:55:04 PM