Go语言基础

一、Go的基本介绍

Go(Golang)语言是Google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言,Go的语法接近C语言,但对于变量的声明有所不同。Go支持垃圾回收功能。

当前有两个Go编译器分支,分别为官方编译器gc和gccgo。官方编译器在初期使用C写成,后用Go重写从而实现自举。Gccgo是一个使用标准GCC作为后端的Go编译器。

官方编译器支持跨平台编译(但不支持CGO),允许将源代码编译为可在目标系统、架构上执行的二进制文件。

博主本人还是很喜欢Go语言的

二、GO的基础知识

注释:

// 单行注释

/**/ 多行注释

标识符:

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

Go语言有37个保留字:

类型 包含保留字
Constants true false iota nil
Types int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 uintptr float32 float64 complex128 complex64 bool byte rune string error
Functions make len cap new append copy close delete complex real imag panic recover

1、变量声明

  1. 变量的来历

    程序运行过程中的数据都是保存在内存中的,通过变量来拿到。

  2. 变量类型

    变量(Variable)的功能是存储数据。常见的变量的数据类型有:整形、浮点型、布尔型等。

    Go语言中的每一个变量都有自己的类型,并且变量必须经过声明才能开始使用,这个和c还有java很像。

  3. 变量的声明

    Go语言中的变量需要声明后才能使用,同一作用域内不支持重复声明。并且Go语言的变量声明后必须使用。

    • 标准声明

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

      var 变量名 变量类型
      

      变量声明以关键字var开头,变量类型放在变量的后面,行尾无需分号。

      var name string
      var age int
      var isOK bool
      
    • 批量声明

      每声明一个变量就需要写var关键字会比较繁琐,go语言中还支持批量变量的声明

      var(
      	a string
          b int
          c bool
          d float32
      )
      
    • 变量的初始化

      Go语言在声明变量的时候,会自动对变量对应的内存区域进行初始化操作。每个变量会被初始化成其类型的默认值,例如:整形和浮点类型变量的默认值为0。字符串变量的默认值为空字符串。布尔类型莫认为false。切片、函数、指针变量默认为nil

      也可以在声明变量的时候为其指定初始值。变量初始化的标准格式如下:

      var 变量名 类型 = 表达式
      // 例
      var username string = "eric"
      var age int = 18
      

      也能一次初始化多个变量

      var name, age = "eric", 18
      
    • 类型推导

      有时候我们会将变量的类型省略,这个时候编译器会根据等号右边的值来推导变量的类型完成初始化。

      var name = "eric"
      var age = 18
      
    • 短变量声明

      在函数内部,可以使用更简略的:=方式声明并初始化变量。

      package main
      
      import ("fmt")
      // Go语言函数外部的语句必须以关键字开头
      // 全局变量
      var m = 100
      const(
      	num = 18
      )
      // main函数是入口函数,他没有参数也没有返回值
      func main(){
          // 函数内部定义的变量必须使用
          n := 10
       m := 200  //此处声明局部变量m
          fmt.PrintLn(m,n)
      }
      
      
    • 匿名变量

      在使用多重赋值的时候,如果想要忽略某个值,可以使用匿名变量(anonymous variable)。匿名变量用一个下划线_表示,例如:

      func foo() (int, string){
           return 18, "eric"
      }
      func main() {
          x,_ := foo()
          _,y := foo()
          fmt.Println("x=", x)
          fmt.Println("y=", y)
      }
      

      匿名变量不占命名空间,不会分配内存,所以匿名变量之间不存在重复声明。(在Lua等变编程语言里,匿名变量也被叫做哑元变量。)

    • 注意事项:

      函数外的每个语句都必须以关键字开始(var、const、func等)

      1. :=不能使用在函数外。
      2. _多用于占位,表示忽略值。
      3. go语言中变量声明必须使用,不适用就编译不过去
      4. 同一个作用域({})中不能重复声明同名的变量
  4. 总结:

    变量声明的三种方式:

    • var name1 string = "Hashflag"
    • var name2 = "HashFlag"
    • 函数内部专属:name3 := "HashFlag"

2、常量

相对于变量,常量是恒定不变的值,多用于定义程序运行期间不会改变的那些值。常量的声明和变量的声明非常类似,只是把var换成了const,常量在定义的时候必须赋值。

const pi = 3.14159275453
const e = 2.7182

声明了pi e 这两个常量之后,在整个程序运行期间他们的值都不能再发生变化了。多个常量也可以一起声明:

const(
	pi = 3.14159275453
    e = 2.7182
)

const同时声明多个常量时,如果省略了值则表示和上面一行的值相同。例如:

const(
	n1 = 100
    n2
    n3
)
// n1,n2,n3的值都是100

3、iota

使用_跳过某些值

const(
	n1 = iote // 第一次默认为0
    n2        // 1
    _
    n4        //3
)

iota声明中间插队

const(
	n1 = iota // 0
    n2 = 100  // 100
    n3 = iota // 2
    n4        // 3
)
const n5 = iota // 0

定义数量级(这里的<<表示左移操作,1<<10表示将1的二进制向左移10位,也就是由1变成了10000000000,也就是十进制的1024。同理2<<2表示将2的二进制向左移2位,也就是由10变成了1000,也就是十进制的8。)

const(
	_ = iota
    KB = 1<<(10*iota)
    MB = 1<<(10*iota)
    GB = 1<<(10*iota)
    TB = 1<<(10*iota)
    PB = 1<<(10*iota)
)

多个iota定义在一行

const(
	a, b = iota + 1, iota + 2  // 1,2
    c, d                       // 2,3
    e, f                       // 3,4
)

4、fmt格式化

package main 
// fmt占位符
func main(){
    var n = 100
    // 查看类型
    fmt.Printf("%T\n", n)  // 查看数据类型
    fmt.Printf("%v\n", n)  // 查看变量值
    fmt.Printf("%b\n", n)  // 二进制
    fmt.Printf("%d\n", n)  // 十进制
    fmt.Printf("%o\n", n)  // 八进制
    fmt.Printf("%x\n", n)  // 十六进制
    var s = "word"
    fmt.Printf("%s\n", s)  // 字符串 word  不会体现类型
    fmt.Printf("%v\n", s)  // 字符串 word  不会体现类型
    fmt.Printf("%#v\n", s)  // 字符串 "word"  体现出了具类型
}

三、Go的基本数据类型

Go语言中有丰富的数据类型,除了基本的整形、浮点型、布尔型、字符串外,还有数组、切片、结构体、函数、map、通道(channel)等。Go语言的基本数据类型和其它语言大同小异。

1、整形

基本整形

整形分为以下两个大类:

  • 按长度分为:int8、int16、int32、int64
  • 对应的无符号整形:uint8、uint16、uint32、uint64

其中,uint8是我们常用和熟知的byte型,int16对应C语言中的shot型(C:短整型),int64对应C语言中的long型(C:长整型)。

类型 描述
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)

特殊整形

类型 描述
uint 32位操作系统上就是uint32,64位操作系统上就是uint64
int 32位操作系统上就是int32,64位操作系统上就是int64
uintptr 无符号整型,用于存放一个指针

注意: 在使用intuint类型时,不能假定它是32位或64位的整型,而是考虑intuint可能在不同平台上的差异。

注意事项:获取对象的长度的内建len()函数返回值的长度可以根据不同平台的字节长度进行变化。实际使用中,切片或map的元素数量等都可以用int来表示。在涉及到二进制传输、读写文件的结构描述时,为了保持文件的结构不会受到不同编译目标平台的字节长度影响,不要使用intuint

数字字面量语法(Number literals syntax)

Go1.13版本之后引入了数字字面量语法,这样便于开发者以二进制、八进制或十六进制浮点数的格式定义数字,例如:v:=0b001101101,代表二进制的101101,相当于十进制的45。v:=0o377,代表八进制的377,相当于十进制的255。v:=0x1p-2,代表十六进制的1除以22,也就是0.25。

package main
 
import "fmt"
 
func main(){
	// 十进制
	var a int = 10
	fmt.Printf("%d \n", a)  // 10
	fmt.Printf("%b \n", a)  // 1010  占位符%b表示二进制
 
	// 八进制  以0开头
	var b int = 077
	fmt.Printf("%o \n", b)  // 77
 
	// 十六进制  以0x开头
	var c int = 0xff
	fmt.Printf("%x \n", c)  // ff
	fmt.Printf("%X \n", c)  // FF
}

2、浮点型

Go语言中有两种浮点型数:float32float64。这两种浮点型数据格式遵循IEEE 754标准:flaot32的浮点最大范围约为1.8e308,可以使用一个常量定义:mathMaxFloat64

打印浮点数时,可以使用fmt包配合动词%f,代码示例:

package main
import (
    "fmt"
    "math"
)
func main() {
    fmt.Printf("%f\n", math.Pi)
    fmt.Printf("%.2f\n", math.Pi)
    // 浮点数
	asd := 3.1415926
	fmt.Printf("%T\n", asd) // 默认Go语言中的小数都是float64类型
	asf := float32(3.14159)
	fmt.Printf("%T\n", asf) // 显示声明float32类型
	// asd = asf  // float32类型的值不能直接赋值给float64,反之也不行。
}

3、复数

两种数据类型complate64和complate128

package main
import (
    "fmt"
    "math"
)
func main() {
    var c1 complex64
    c1 = 1 + 2i
    var c2 complex128
    c2 = 2 + 3i
    fmt.Println(c1)
    fmt.Println(c2)
}

4、布尔值

Go语言中以bool类型进行声明布尔类型数据,布尔类型数据只有true(真)falde(假)两个值。

注意:

  1. 布尔类型变量的默认值为false
  2. Go语言中不允许将整形强制转换为布尔型。
  3. 布尔型无法参予数值运算,也无法与其它类型进行转换。

5、字符串

注:字符串默认不可修改

Go语言中的字符串以原生数据类型出现,使用字符串就像使用其他原生数据类型(int、bool、float32、float64等)一样。Go语言里的字符串的内部实现使用UTF-8编码。字符串的值为双引号(")中的内容,可以在Go语言的源码中直接添加非ASCII码字符,例如:

s1 := "hello"
s2 := "你好"

字符串转义符

Go语言的字符串常见转义符包含回车、换行、单双引号、制表符等,如下表格所示。

转义符 含义
\r 回车符(返回行首)
\n 换行符(直接跳到下一行的同列位置)
\t 制表符
\' 单引号
\" 双引号
\\ 反斜杠

举个例子,我们要打印一个Windows平台下的一个文件路径:

package main
import (
    "fmt"
)
func main() {
    str := "\"c:\\User\\AppDate\\.go\""
    fmt.Println(str)  // 双引号需要转换
    fmt.Println(`c:\User\AppDate\.go\`)  // 反引号原样输出
}

多行字符串

Go语言中要定义一个多行字符串时,就必须使用反引号字符:

package main
import (
    "fmt"
)
func main() {
	asd := `第一行
第二行
第三行`
	fmt.Println(asd)
}

反引号间换行将被作为字符串中的换行,但是所有的转义字符均无效,文本将会原样输出。

字符串的常用操作

方法 介绍
len(str) 求长度
+或fmt.Sprintf 拼接字符串
strings.Split 分割
strings.Contains 判断是否包含
strings.HasPrefix,strings.HasSuffix 前缀/后缀判断
strings.Index(),strings.LastIndex() 子串出现的位置
strings.Join(a[]string, sep string) join操作

具体示例:

package main

import (
	"fmt"
	"strings"
)

func main() {
	strs1 := "Hello world!"
	fmt.Printf("%s %d\n", strs1, len(strs1))
	strs2 := "Hi Griles!"
	fmt.Printf("%s\n", strs1+strs2)
	strs3 := fmt.Sprintf("%s%s\n", strs1, strs2)
	fmt.Printf("%s\n", strs3)
	strs4 := strings.Split(strs3, "i")
	fmt.Printf("%s\n", strs4)
	strs5 := strings.Contains(strs2, "i")
	fmt.Println(strs5)
    strs6 := strings.HasPrefix(strs2, "H")
	strs7 := strings.HasSuffix(strs2, "S")
	fmt.Println(strs6, strs7)
    strs8 := strings.Index(strs2, "Hi")
	strs9 := strings.LastIndex(strs2, "les")
	fmt.Println(strs8, strs9)
	strs10 := strings.Join([]string{"1", "2", "3"}, "+")
	fmt.Println(strs10)
}

byte和rune类型

组成每个字符串的元素叫做“字符“,可以通过遍历或者单个获取字符串元素获得字符。字符用单引号('')包裹起来,像:

var a = '汉'
var b = 'x'

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

  1. unit8类型,或者叫byte型,代表了ASCII码的一个字符。
  2. rune类型,代表一个utf-8字符。

当需要处理中文、日文或者其它符合字符时,需要用到rune类型。rune类型实际是一个int32

Go使用了特殊的runne类型来处理Unicode,让基于Unicode的文本处理更为方便,也可以使用byte类型进行默认字符处理,性能和扩展性都有照顾。

示例:

// 遍历字符串
func traversalString() {
	s := "hello沙河"
	for i := 0; i < len(s); i++ { //byte
		fmt.Printf("%v(%c) ", s[i], s[i])
	}
	fmt.Println()
	for _, r := range s { //rune
		fmt.Printf("%v(%c) ", r, r)
	}
	fmt.Println()
}

输出:

104(h) 101(e) 108(l) 108(l) 111(o) 230(æ) 178(²) 153() 230(æ) 178(²) 179(³) 
104(h) 101(e) 108(l) 108(l) 111(o) 27801(沙) 27827(河) 

因为UTF8编码下一个中文汉字由3~4个字节组成,所以我们不能简单的按照字节去遍历一个包含中文的字符串,否则就会出现上面输出的结果。

字符串底层是一个byte数组,所以可以和[]byte类型相互转换,字符串是不能修改的,字符串是由byte字节组成,所以字符串长度是byte字节的长度。rune类型用来表示utf8字符,一个rune字符由一个或多个byte组成。

修改字符串

要修改字符串,需要先将其转换成[]runne[]byte,完成后再转换为string。无论哪种转换,都会重新分配内存,并复制字节数组。

package main

import (
	"fmt"
)

// 浮点数
func main() {
	asd := "Hello world"
	dsa := "欸瑞克"
	fmt.Printf("asd: %T dsa:%T\n", asd, dsa)
	asd1 := []rune(asd) // 把字符串强制转换成一个rune切片
	fmt.Printf("asd1: %T dsa:%d\n", asd1, asd1)
	fmt.Printf("asd1: %T dsa:%s\n", asd1, string(asd1)) // 把runne切片强制转换成字符串
	asd1[0] = 'h'
	fmt.Printf("asd1: %T dsa:%d\n", asd1, asd1)
	fmt.Printf("asd1: %T dsa:%s\n", asd1, string(asd1))
	asd1[0] = 'h'
	fmt.Printf("asd1: %d\n", asd1[0])
	asd2 := "H"       // string类型
	asd3 := byte('H') // byte(uint8)类型
	asd4 := "是"       // string类型
	asd5 := '是'       // rune(int32)类型
	fmt.Printf("%T %T %T %T\n", asd2, asd3, asd4, asd5)
}

6、类型转换

Go语言中只有强制类型转换,没有隐式类型转换。该语法只能在两个类型之间支持相互转换的时候使用。

强制类型转换的基本语法如下:

T(表达式)

其中,T表示要转换的类型。表达式包括变量、复杂算子和函数返回值等。

比如计算直角三角形的斜边长时使用math包的Sqrt()函数,该函数接收的是float64类型的参数,而变量a和b都是int类型的,这个时候就需要将a和b强制转换为float64类型。

func sqrtDemo() {
	var a, b = 3, 4
	var c int
	// math.Sqrt()接收的参数是float64类型,需要强制转换
	c = int(math.Sqrt(float64(a*a + b*b)))
	fmt.Println(c)
}

四、运算符

运算符用于在程序运行时执行数学或逻辑运算。

Go 语言内置的运算符有:

  1. 算术运算符
  2. 关系运算符
  3. 逻辑运算符
  4. 位运算符
  5. 赋值运算符

算术运算符

运算符 描述
+ 相加
- 相减
* 相乘
/ 相除
% 求余

注意: ++(自增)和--(自减)在Go语言中是单独的语句,并不是运算符。

关系运算符

运算符 描述
== 检查两个值是否相等,如果相等返回 True 否则返回 False。
!= 检查两个值是否不相等,如果不相等返回 True 否则返回 False。
> 检查左边值是否大于右边值,如果是返回 True 否则返回 False。
>= 检查左边值是否大于等于右边值,如果是返回 True 否则返回 False。
< 检查左边值是否小于右边值,如果是返回 True 否则返回 False。
<= 检查左边值是否小于等于右边值,如果是返回 True 否则返回 False。

逻辑运算符

运算符 描述
&& 逻辑 AND 运算符。 如果两边的操作数都是 True,则为 True,否则为 False。
|| 逻辑 OR 运算符。 如果两边的操作数有一个 True,则为 True,否则为 False。
! 逻辑 NOT 运算符。 如果条件为 True,则为 False,否则为 True。

位运算符

位运算符对整数在内存中的二进制位进行操作。

运算符 描述
& 参与运算的两数各对应的二进位相与。 (两位均为1才为1)
| 参与运算的两数各对应的二进位相或。 (两位有一个为1就为1)
^ 参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1。 (两位不一样则为1)
<< 左移n位就是乘以2的n次方。 “a<<b”是把a的各二进位全部左移b位,高位丢弃,低位补0。
>> 右移n位就是除以2的n次方。 “a>>b”是把a的各二进位全部右移b位。

赋值运算符

运算符 描述
= 简单的赋值运算符,将一个表达式的值赋给一个左值
+= 相加后再赋值
-= 相减后再赋值
*= 相乘后再赋值
/= 相除后再赋值
%= 求余后再赋值
<<= 左移后赋值
>>= 右移后赋值
&= 按位与后赋值
|= 按位或后赋值
^= 按位异或后赋值

五、流程控制

Go语言中常用的流程控制有iffor,而switchgoto主要为了简化代码、降低重复代码而生成的结构,属于扩展类的流程控制。

注:Go语言中没有while循环,但是在Go语言中可以实现while的效果,通过for true来实现

1、if else(分支结构)

if条件判断的基本写法

Go语言中if条件判断的格式如下:

if 表达式1 {
    分支1
} else if 表达式2 {
    分支2
} else {
    分支3
}

通过if进行判断,当表达式1的结果为true时执行分支1;否则继续进行else if判断表达式2结果为true时执行分支2;表达式2的结果为false时执行else;if判断中的逻辑是先进行if判断如果后面有else if 就进行else if判断,没有就往下进行else判断。具体情况具体分析,灵活判断。

Go语言规定if匹配的左括号{必须与if和表达式放在同一行,{放在其他位置会触发编码错误。同理,与else匹配的{else if匹配的{都要在同一行。

if判断条件的特殊写法

if条件还有一种特殊的写发,可以在if表达式之前添加一个执行语句,再根据变量值进行判断,例如:

func demo() {
    if score := 5; score >= 8{
        fmt.Println("A")
    }else if score > 6 && score  {
        fmt.Println("B")
    }else {
        fmt.Println("C")
    }
}

普通写法:

func demo() {
    score := 65
    if score >= 90 {
        fmt.Println("A")
    } else if score > 75 && score < 90 {
        fmt.PrintLn("B")
    } else {
        fmt.Println("C"))
    }
}

2、for循环结构

Go语言中的所有循环类型均可使用for关键字来完成。

for 循环的基本格式:

for 初始语句;条件表达式;结束语句{
	循环体语句
}

条件表达式返回true时循环体不停地进行循环,直到条件表达式返回false时自动退出循环。

func demo(){
    for i := 0;i < 10; i++ {
        fmt.Println(i)
    }
}

for循环的初始语句可以被忽略,但是初始语句后的分号必须要写:

func demo() {
    i := 0
    for ; i < 10; i++ {
        fmt.Println(i)
    }
}

for循环的初始语句和结束语句都可以省略:

func demo() {
    i := 0
    for i < 10 {
        fmt.Println(i)
        i++
    }
}

这种写法类似于其它编程语言中的while,在while后添加一个条件表达式,满足条件表达式时持续循环,否则结束循环

无限循环

for {
	循环体语句
}
或
for true {
	循环体语句
}

for循环可以通过breakgotoreturnpanic语句强制退出循环。

3、for range(键值循环)

Go语言中可以使用for range遍历数组、切片、字符串、map及通道(channel)。通过for range遍历的返回值有以下规律:

  1. 数组、切片、字符串返回索引和值。
  2. map返回键和值。
  3. 通道(channel)只返回通道内的值。

4、switch case

使用switch语句可方便地对大量数据值进行条件判断。

func demo() {
    finger := 3
    switch finger {
	case 1:
		fmt.Println("猜错了")
	case 2:
		fmt.Println("猜少了")
	case 3:
		fmt.Println("回答正确")
	case 4:
		fmt.Println("猜多了")
	case 5:
		fmt.Println("猜错了")
	default:
		fmt.Println("无效的输入!")
	}
}

Go语言规定每个switch只能有一个default分支。

一个分支可以有多个值,多个case值中间使用英文逗号分隔。

func demo() {
    switch n := 7; n{
    case 1, 3, 5, 7, 9:
	    fmt.Println("奇数")
    case 2, 4, 6, 8:
    	fmt.Println("偶数")
    default:
        fmt.Println(n)
    }
}

分支也可以使用表达式:

func demo() {
    age := 30
    switch {
    case age < 25:
        fmt.Println("不及格!")
    case age > 25 && age < 35:
        fmt.Println("继续努力!")
    case age > 60:
        fmt.Println("及格")
    default:
        fmt.Println("良好")
    }
}

fallthrough语法可以执行满足条件的case的下一个case,是为了兼容C语言中的case设计的。

func demo() {
    s := "a"
    switch {
    case s == "a":
        fmt.Println("a")
        fallthrough
    case s == "b":
        fmt.Println("b")
    case s == "c":
        fmt.Println("c")
    default:
        fmt.Println("...")
    }
}

输出:

a
b

5、goto(跳转到指定标签)

goto语句通过标签进行代码间的无条件跳转。goto语句可以在快速跳出循环、避免重复退出上有一定的帮助。Go语言中使用goto语句能简化一些代码的实现过程。例如双层嵌套的for循环要退出时:

func demo() {
    var breakFlag bool
    for i := 0; i < 10; i++ {
        if j := 0; j < 10; j++ {
            if j == 2 {
            	// 设置推出标签
            	breakFlag = true
            	break  
            }
            fmt.Printf("%v-%v\n", i, j)
        }
        // 外层for循环判断
    	if breakFlag {
        	break
    	}
    }
}

使用goto简化代码:

func demo() {
    for i := 0; i < 10; i++ {
        for j := 0; j < 10; j++ {
            if j == 2 {
                // 设置退出标签
                goto breakTag
            }
            fmt.Printf("%v-%v\n", i, j)
        }
    }
    return
// 标签
breakTag:
    fmt.Println("结束for循环")
}

6、break(跳出循环)

break语句可以结束forswitchselect的代码块。

break语句还可以在语句后面添加标签,表示退出某个标签对应的代码块,标签要求必须定义在对应的forswitchselect的代码块上。举个例子:

func demo() {
BREAKDEMO:
    for i := 0; i < 10; i++ {
		for j := 0; j < 10; j++ {
			if j == 2 {
				break BREAKDEMO
			}
			fmt.Printf("%v-%v\n", i, j)
		}
	}
	fmt.Println("...")
}

7、continue(继续下次循环)

continue语句可以结束当前循环,开始下一次的循环迭代过程,仅限在for循环内使用。

continue语句后添加标签时,表示开始标签对应的循环。例:

func demo() {
forloop:
	for i := 0; i < 5; i++ {
		// forloop2:
		for j := 0; j < 5; j++ {
			if i == 2 && j == 2 {
				continue forloop
			}
			fmt.Printf("%v-%v\n", i, j)
		}
	}
}

六、数组

Arrary(数组)

数组是同意种数据类型元素的集合。在Go语言中,数组从声明时就确定,使用时可以修改数组成员,但是数组大小不可变化。基本语法:

// 定义一个长度为3的元素类型为int的数组a
var a [3]int

1、数组定义

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

注:数组的长度必须是常量,并且长度是数组类型的一部分。一旦定义,长度不能变。[5]int[10]int是不同类型。

var a [3]int
var b [4]int
a = b // 不可以这样做,因为此时的a和b是不同的类型

数组可以通过下标进行访问,下标是从0开始,最后一个元素下标是:len-1,访问越界(下标在合法范围之外),则触发访问越界,会panic。

2、数组的初始化

数组的初始化方式有很多种

方式一

初始化数组时可以使用初始化列表来设置数组元素的值。

func main() {
	var testArray [3]int                        //数组会初始化为int类型的零值
	var numArray = [3]int{1, 2}                 //使用指定的初始值完成初始化
	var cityArray = [3]string{"北京", "上海", "深圳"} //使用指定的初始值完成初始化
	fmt.Println(testArray)                      //[0 0 0]
	fmt.Println(numArray)                       //[1 2 0]
	fmt.Println(cityArray)                      //[北京 上海 深圳]
}

方式二

除了每次确保提供初始值和数组长度一致,一般情况下我们可以让编译器根据初始值的个数自行推断数组的长度,例如:

func main(){
    var testArray [3]int
	var numArray = [...]int{1, 2}
	var cityArray = [...]string{"北京", "上海", "深圳"}
	fmt.Println(testArray)                          //[0 0 0]
	fmt.Println(numArray)                           //[1 2]
	fmt.Printf("type of numArray:%T\n", numArray)   //type of numArray:[2]int
	fmt.Println(cityArray)                          //[北京 上海 深圳]
	fmt.Printf("type of cityArray:%T\n", cityArray) //type of cityArray:[3]string
}

方式三

我们还可以使用指定索引值的方式来初始化数组,例如:

func main(){
    a := [...]int{1: 1, 3: 5}
	fmt.Println(a)                  // [0 1 0 5]
	fmt.Printf("type of a:%T\n", a) //type of a:[4]int 
}

3、数组遍历

遍历数组a有以下两种方式:

  • 方式一:for循环遍历

    func main() {
    	var a = [...]string{"北京", "上海", "深圳"}
    	// 方法1:for循环遍历
    	for i := 0; i < len(a); i++ {
    		fmt.Println(a[i])
    	}
    }
    
  • 方式二:for range遍历

    func main() {
    	var a = [...]string{"北京", "上海", "深圳"}
    	// 方法2:for range遍历
    	for index, value := range a {
    		fmt.Println(index, value)
    	}
    }
    

4、多维数组

Go语言是支持多维数组的,我们这里以二维数组为例(数组中又嵌套数组)。

二维数组的定义

func main(){
    a := [3][2]string{
		{"北京", "上海"},
		{"广州", "深圳"},
		{"成都", "重庆"},
	}
	fmt.Println(a) // [[北京 上海] [广州 深圳] [成都 重庆]]
	fmt.Println(a[2][1]) // 支持索引取值:重庆
}

二维数组的遍历

func main(){
    a := [3][2]string{
		{"北京", "上海"},
		{"广州", "深圳"},
		{"成都", "重庆"},
	}
	for _, v1 := range a {
		for _, v2 := range v1 {
			fmt.Printf("%s\t", v2)
		}
		fmt.Println()
	}
}

输出:

北京	上海	
广州	深圳	
成都	重庆

注意:多维数组只有第一层可以使用...来让编译器推到数组长度。例如:

// 支持的写法
a := [...][2]string{
	{"北京", "上海"},
	{"广州", "深圳"},
	{"成都", "重庆"},
}
// 不支持多维数组的内层使用...
b := [3][...]string{
	{"北京", "上海"},
	{"广州", "深圳"},
	{"成都", "重庆"},
}

5、数组是值类型

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

func modifyArray(x [3]int) {
	x[0] = 100
}

func modifyArray2(x [3][2]int) {
	x[2][0] = 100
}
func main() {
	a := [3]int{10, 20, 30}
	modifyArray(a) //在modify中修改的是a的副本x
	fmt.Println(a) //[10 20 30]
	b := [3][2]int{
		{1, 1},
		{1, 1},
		{1, 1},
	}
	modifyArray2(b) //在modify中修改的是b的副本x
	fmt.Println(b)  //[[1 1] [1 1] [1 1]]
}

注意:

  1. 数组支持“==”、"!="操作符,因为内存总是被初始化的。
  2. [n]*T表示指针数组,*[n]T表示数组指针。

七、切片

数组的长度是固定的并且数组长度属于类型的一部分,所以数组有很多的局限性。例如:

func arraySum(x [3]int) int{
    sum := 0
    for _, v := range x{
        sum = sum + v
    }
    return sum
}

arraySum函数只能接收[3]int类型,其他的都不支持。比如:

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

数组a中已经有三个元素了,我们不能再继续往数组a中添加新元素。

1、切片的概念

切片(Slice)是一个拥有相同元素的可变长度的序列。它是基于数组类型做的一层封装。非常灵活,支持自动扩容。

切片是一个引用类型,它内部结构包含地址长度容量。切片一般用于快速的操作一块数据集合。

2、切片的定义

声明切片类型的基本语法:

var name []T
  • name:表示变量名
  • T:表示切片中的元素类型

例:

func main(){
    // 声明切片类型
	var a []string              //声明一个字符串切片
	var b = []int{}             //声明一个整型切片并初始化
	var c = []bool{false, true} //声明一个布尔切片并初始化
	var d = []bool{false, true} //声明一个布尔切片并初始化
	fmt.Println(a)              //[]
	fmt.Println(b)              //[]
	fmt.Println(c)              //[false true]
	fmt.Println(a == nil)       //true
	fmt.Println(b == nil)       //false
	fmt.Println(c == nil)       //false
	// fmt.Println(c == d)   //切片是引用类型,不支持直接比较,只能和nil比较
}

切片的长度和容量

切片拥有自己的长度和容量,我们可以通过使用内置的len()函数求长度,使用内置的cap()函数求切片的容量。

切片表达式

切片表达式从字符串、数组、指向数组或切片的指针构造子字符串或切片。它有两种变体:一种是指定low和high两个索引界限值的简单的形式,另一种是除了low和high索引界限值外还指定容量的完整的形式。

简单切片表达式

切片的底层就是一个数组,所以我们可以基于数组通过切片表达式得到切片。切片表达式中的lowhight表示一个索引范围(左包含,右不包含),也就是下面代码中从数组a中选出1<=索引值<4的元素组成切片s,得到的切片长度=high-low,容量等于得到的切片的底层数组的容量。

func main() {
	a := [5]int{1, 2, 3, 4, 5}
	s := a[1:3]  // s := a[low:high]
	fmt.Printf("s:%v len(s):%v cap(s):%v\n", s, len(s), cap(s))
}

输出:

s:[2 3] len(s):2 cap(s):4

为了方便起见,可以省略切片表达式中的任何索引。省略了low则默认为0;省略了high则默认为切片操作数的长度:

a[2:]  // 等同于 a[2:len(a)]
a[:3]  // 等同于 a[0:3]
a[:]   // 等同于 a[0:len(a)]

注意:

对于数组或字符串,如果0 <= low <= high <= len(a),则索引合法,否则就会索引越界(out of range)。

对切片在执行切片表达式时(切片再切片),high的上限边界是切片的容量cap(a),而不是长度。常量索引必须在有效范围内。如果lowhigh两个指标都是常量,它们必须满足low<=high。如果索引在运行时超出范围,就会发生运行时panic

func main(){
    a := [5]int{1, 2, 3, 4, 5}
	s := a[1:3]  // s := a[low:high]
	fmt.Printf("s:%v len(s):%v cap(s):%v\n", s, len(s), cap(s))
	s2 := s[3:4]  // 索引的上限是cap(s)而不是len(s)
	fmt.Printf("s2:%v len(s2):%v cap(s2):%v\n", s2, len(s2), cap(s2))
}

输出:

s:[2 3] len(s):2 cap(s):4
s2:[5] len(s2):1 cap(s2):1

完整切片表达式

对于数组,指向数组的指针,或切片a(注意不能是字符串)支持完整切片表达式:

a[low : high : max]

上面的代码会构造与简单切片表达式a[low: high]想、相同类型、相同长度和元素的切片。另外,它会将得到的结果切片的容量设置为max-low。在完整切片表达式中只有第一个索引值(low)可以省略;它默认为0。

func main() {
	a := [5]int{1, 2, 3, 4, 5}
	t := a[1:3:5]
	fmt.Printf("t:%v len(t):%v cap(t):%v\n", t, len(t), cap(t))
}

输出结果:

t:[2 3] len(t):2 cap(t):4

完整切片表达式需要满足的条件是0 <= low <= high <= max <= cap(a),其他条件和简单切片表达式相同。

使用make()函数构造切片

我们上面都是基于数组来创建的切片,如果需要动态创建一个切片,我们就需要使用内置的make()函数,格式如下:

make([]T, size, cap)
  • T:切片的元素类型
  • size:切片中元素的数量
  • cap:切片的容量

例:

func main() {
	a := make([]int, 2, 10)
	fmt.Println(a)      //[0 0]
	fmt.Println(len(a)) //2
	fmt.Println(cap(a)) //10
}

main函数中a的内部存储空间已经分配了10个,但实际上只用了2个,容量并不会影响当前元素的个数,所以len(a)返回2,cap(a)则返回该切片的容量

切片的而本质

切片的本质就是随底层数组的封装,它包含了三个信息:底层数组的指针、切片的长度(len)和切片的容量(cap)。

判断切片是否为空

要检查切片是否为空,需要使用len(s)==0来判断,不要使用s == nil来判断。

3、切片不能直接比较

切片之间是不能比较的,我们不能使用==操作符来判断两个切片是否含有全部相等元素。切片唯一合法的比较是和nil比较。一个nil值的切片并没有底层数组,一个nil值的切片的长度和容量都是0。但是不能说一个长度和容量都是0的切片一定是nil,因为切片初始化为空值的时候的长度和容量也都是0,但不等于nil

var s1 []int         //len(s1)=0;cap(s1)=0;s1==nil
s2 := []int{}        //len(s2)=0;cap(s2)=0;s2!=nil
s3 := make([]int, 0) //len(s3)=0;cap(s3)=0;s3!=nil

所以判断切片是否为空,要用len(s)==0来判断,不应该使用s == nil来判断

4、切片的赋值拷贝

拷贝前后两个变量共享底层数组,对一个切片的修改会影响零一个切片的内容,需要注意。

func main() {
	s1 := make([]int, 3) //[0 0 0]
	s2 := s1             //将s1直接赋值给s2,s1和s2共用一个底层数组
	s2[0] = 100
	fmt.Println(s1) //[100 0 0]
	fmt.Println(s2) //[100 0 0]
}

5、切片遍历

切片的遍历方式和数组一样,支持索引比哪里和for range遍历。

func main() {
	s := []int{1, 3, 5}

	for i := 0; i < len(s); i++ {
		fmt.Println(i, s[i])
	}

	for index, value := range s {
		fmt.Println(index, value)
	}
}

6、append()方法为切片添加元素

Go语言的内建函数append()可以为切片动态添加元素。可以一次添加一个元素,可以添加多个元素,也可以添加另一个切片中的元素(否面加...)。

func main(){
	var s []int
	s = append(s, 1)        // [1]
	s = append(s, 2, 3, 4)  // [1 2 3 4]
	s2 := []int{5, 6, 7}  
	s = append(s, s2...)    // [1 2 3 4 5 6 7]
}

注意:通过var声明的零值切片可以在append()函数直接使用,无需初始化。

var s []int
s = append(s, 1, 2, 3)

没有必要初始化一一个切片再传入append()函数使用。

s := []int{}  // 没有必要初始化
s = append(s, 1, 2, 3)

var s = make([]int)  // 没有必要初始化
s = append(s, 1, 2, 3)

每个切片会指向一个底层数组,这个数组的容量够用就添加新元素。当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行“扩容”,此时该切片指向的底层数组就会更换。“扩容”操作往往发生在append函数调用时,所以我们通常都需要用原变量接收append函数的返回值。

例:

func main() {
	//append()添加元素和切片扩容
	var numSlice []int
	for i := 0; i < 10; i++ {
		numSlice = append(numSlice, i)
		fmt.Printf("%v  len:%d  cap:%d  ptr:%p\n", numSlice, len(numSlice), cap(numSlice), numSlice)
	}
}

输出:

[0]  len:1  cap:1  ptr:0xc0000a8000
[0 1]  len:2  cap:2  ptr:0xc0000a8040
[0 1 2]  len:3  cap:4  ptr:0xc0000b2020
[0 1 2 3]  len:4  cap:4  ptr:0xc0000b2020
[0 1 2 3 4]  len:5  cap:8  ptr:0xc0000b6000
[0 1 2 3 4 5]  len:6  cap:8  ptr:0xc0000b6000
[0 1 2 3 4 5 6]  len:7  cap:8  ptr:0xc0000b6000
[0 1 2 3 4 5 6 7]  len:8  cap:8  ptr:0xc0000b6000
[0 1 2 3 4 5 6 7 8]  len:9  cap:16  ptr:0xc0000b8000
[0 1 2 3 4 5 6 7 8 9]  len:10  cap:16  ptr:0xc0000b8000

结论:

  1. append()函数将元素追加到切片的最后并返回该切片。
  2. 切片numSlice的容量按照1,2,4,8,16这样的规则自动进行扩容,每次扩容后都是扩容前的2倍。

append()函数支持一次性追加多个元素。

var citySlice []string
// 追加一个元素
citySlice = append(citySlice, "北京")
// 追加多个元素
citySlice = append(citySlice, "上海", "广州", "深圳")
// 追加切片
a := []string{"成都", "重庆"}
citySlice = append(citySlice, a...)
fmt.Println(citySlice) //[北京 上海 广州 深圳 成都 重庆]

7、切片的扩容策略

可以通过查看$GOROOT/src/runtime/slice.go源码,其中扩容相关代码如下:

newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
	newcap = cap
} else {
	if old.len < 1024 {
		newcap = doublecap
	} else {
		// Check 0 < newcap to detect overflow
		// and prevent an infinite loop.
		for 0 < newcap && newcap < cap {
			newcap += newcap / 4
		}
		// Set newcap to the requested cap when
		// the newcap calculation overflowed.
		if newcap <= 0 {
			newcap = cap
		}
	}
}

总结:

  • 首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)。
  • 否则判断,如果旧切片的长的小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap).
  • 否则判断,如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap)
  • 如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)。

注:

切片扩容还会根据切片中元素的类型不同而做不同的处理,比如intstring类型的处理方式就不一样。

8、使用copy()函数赋值切片

func main() {
	a := []int{1, 2, 3, 4, 5}
	b := a
	fmt.Println(a) //[1 2 3 4 5]
	fmt.Println(b) //[1 2 3 4 5]
	b[0] = 1000
	fmt.Println(a) //[1000 2 3 4 5]
	fmt.Println(b) //[1000 2 3 4 5]
}

由于切片是引用类型,所以a和b其实都指向了同一块内存地址。修改b的同时a的值也会发生变化。

Go语言内建的copy函数可以迅速地将一个切片的数据复制到另一个切片空间中,copy()函数的使用格式:

copy(destSlice, srcSlice []T)

总结:

  • srcSlice:数据来源切片
  • destSlice:目标切片

例:

func main() {
	// copy()复制切片
	a := []int{1, 2, 3, 4, 5}
	c := make([]int, 5, 5)
	copy(c, a)     //使用copy()函数将切片a中的元素复制到切片c
	fmt.Println(a) //[1 2 3 4 5]
	fmt.Println(c) //[1 2 3 4 5]
	c[0] = 1000
	fmt.Println(a) //[1 2 3 4 5]
	fmt.Println(c) //[1000 2 3 4 5]
}

9、从切片中删除元素

Go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素。

func main() {
	// 从切片中删除元素
	a := []int{30, 31, 32, 33, 34, 35, 36, 37}
	// 要删除索引为2的元素
	a = append(a[:2], a[3:]...)
	fmt.Println(a) //[30 31 33 34 35 36 37]
}

删除元素:从切片a中删除索引为index的元素,操作方法是a=append(a[:index], a[index+1:]...)

八、Map

Go语言中提供的映射关系容器为map,其内部使用散列表(hash)实现。

map

map是一种无序的基于key-value的数据结构,Go语言中的map是引用类型,必须初始化才能使用。

map定义

Go语言中map的定义语法如下:

map[KeyType]ValueType
  • KeyType:表示键的类型。
  • ValueType:表示键对应的值的类型。

map类型的变量默认初始值为nil,需要使用make()函数来分配内存。

make(map[KeyType]ValueType, [cap])

cap表示map的容量,该参数虽然不是必须的,但是我们应该在初始化map的时候就为其指定一个合适的容量。

map基本使用

map中的数据都是成对出现的,map的基本使用示例:

func main() {
	scoreMap := make(map[string]int, 8)
	scoreMap["张三"] = 90
	scoreMap["小明"] = 100
	fmt.Println(scoreMap)
	fmt.Println(scoreMap["小明"])
	fmt.Printf("type of a:%T\n", scoreMap)
}

输出:

map[小明:100 张三:90]
100
type of a:map[string]int

map也支持在声明的时候填充元素,例:

func main() {
	userInfo := map[string]string{
		"username": "沙河小王子",
		"password": "123456",
	}
	fmt.Println(userInfo) //
}

判断某个键是否存在

Go语言中有个判断map中键是否存在的特殊写法,格式:

value, ok := map[key]

例:

func main() {
	scoreMap := make(map[string]int)
	scoreMap["张三"] = 90
	scoreMap["小明"] = 100
	// 如果key存在ok为true,v为对应的值;不存在ok为false,v为值类型的零值
	v, ok := scoreMap["张三"]
	if ok {
		fmt.Println(v)
	} else {
		fmt.Println("查无此人")
	}
}

九、函数

函数是组织好的、可复用的、用于执行指定任务的代码块。

Go语言中支持函数、匿名函数和闭包,并且函数在Go语言中属于“一等公民”。

1、函数的定义

Go语言中定义函数使用func关键字,具体格式:

func 函数名(参数)(返回值){
    函数体
}
  • 函数名:由字母、数字、下划线组成。但函数名的第一个字母不能是数字。在同一个包内,函数名不能重名。
  • 参数:参数由参数变量和参数变量的类型组成,多个参数之间使用逗号,分隔。
  • 返回值:返回值由返回值变量和其变量类型组成,也可以只写返回值的类型,多个返回值必须用()包裹,并用,分隔。
  • 函数体:实现指定功能的代码块。

例:

func intSum(x int, y int) int {
    return x + y
}

函数的参数和返回值都是可选的,例如我们可以实现一个既不需要参数也没有返回值的函数:

func sayHellow() {
    fmt.Println("Hello 沙河")
}

2、函数的调用

定义函数后,我们可以通过函数名()的方式调用函数。例:

func main() {
    sayHello()
    ret := intSun(10, 20)
    fmt.Println(ret)
}

注意,调用有返回值的函数时,可以不接收其返回值。

3、参数

类型简写

函数的参数中如果相邻变量的类型相同,则可以省略类型,例:

func intSum(x, y int) int {
    return x + y
}

intSum函数有两个参数,这两个参数的类型均为int,可以省略x的类型,因为y后面有和x一样的类型说明。

可变参数

可变参数是指函数的参数数量不固定。Go语言中的可变参数通过在参数名后加...来标识。

注:可变参数通常要作为函数的最后一个参数。

例:

func intSum(x ...int) int {
    fmt.Println(x) // x是一个切片
    sun := 0
    for _,v := range x {
        sum = sum + v
    }
    return sum 
}

调用上面的函数:

ret1 := intSum()
ret2 := intSum(10)
ret3 := intSum(10, 20)
ret4 := intSum(10, 20, 30)
fmt.Println(ret1, ret2, ret3, ret4) //0 10 30 60

固定参数搭配可变参数使用时,可变参数要放在固定参数的后面,例:

func intSum(x int, y ...int) int {
    fmt.Print(x, y)
    sum := x
    for _, v := range y {
        sum = sum + v
    }
    return sum
}

调用上述函数:

ret5 := intSum3(100)
ret6 := intSum3(100, 10)
ret7 := intSum3(100, 10, 20)
ret8 := intSum3(100, 10, 20, 30)
fmt.Println(ret5, ret6, ret7, ret8) //100 110 130 160

本质上,函数的可变参数是通过切片来实现的。

4、返回值

Go语言中通过return关键字向外输出返回值。

多返回值

Go语言中函数支持多返回值,函数如果有多个返回值时必须用()将所有返回值包裹起来。

func calc(x, y int) (int, int) {
	sum := x + y
	sub := x - y
	return sum, sub
}

返回值命名

函数定义时可以给返回值命名,并在函数体中直接使用这些变量,最后通过return关键字返回。

例:

func calc(x, y int) (sum, sub int){
    sum = x+y
    sub = x-y
    return 
}

注:当一个返回值类型为slice时,nil可以看做是一个有效的slice,没有必要显示返回一个长度为0的切片。

func someFunc(x string) []int {
    if x == "" {
        return nil // 没有必要返回[]int 
    }
}

5、变量的作用域

全局变量

全局变量是定义在函数外部的变量,它在程序整个运行周期内都有效。在函数中可以访问到全局变量。

package main

import "fmt"

// 定义全局变量num
var num int64 = 10

func testGlobalVar(){
    fmt.Printf("num=%d\n", num) // 函数中可以访问全局变量num
} 
func main() {
    testGlobalVar() // num=10
}

局部变量

局部变量又分为两种:函数内定义的变量无法在该函数外使用,例:main函数中无法使用testLocalVar函数中定义的变量x:

func testLocalVar(){
    // 定义一个函数局部变量x,仅在该函数内部生效
    var x int64 = 100
    fmt.Print("x=%d\n", x)
}
func main() {
    testLocalCal()
    fmt.Println(x) // 此时无法使用变量x
}

如果局部变量和全局变量重名,优先访问局部变量。

package main

import "fmt"

// 定义全局变量num
var num int64 = 10

func testNum() {
    num := 100
    fmt.Printf("num=%d\n", num) // 函数优先使用局部变量
}
func main() {
    testNum() // num=100
}

场景:通常在if条件判断、for循环、switch语句上使用这种定义变量的方式。

func testLocalVar2(x, y int) {
    fmt.Println(x, y) // 函数的参数也是只在本函数中生效
    if x > 0 {
        z := 100 // 变量z只在if语句块生效
        fmt.Println(z)
    }
    // fmt.Println(z) // 此处无法使用变量z
}

6、函数类型与变量

定义含数类型

可以使用type关键字来定义一个函数类型,例:

type calculation function(int, int) int

上面定义了一个calculation类型,它是一种数据类型,这种数据类型接收两个int类型的参数并且返回一个int类型的返回值。

也就是说,凡是发满足当前条件的calculation类型的函数,例如下面的add和sub是calculation类型。

func add(x, y int) int {
    return x + y
}

func sub(x, y int) int {
    return x - y
}

add和sub都能赋值给calculation类型的变量。

var c calculation
c = add

函数类型变量

可以声明函数类型的变量并为该变量赋值:

func main() {
    var c calculation                 // 声明一个calculation类型的变量c
    c = add                           // 把add赋值给c
    fmt.Printf("type of c:%T\n", c)   // type of c:main.calculation
    fmt.Println(c(1,2))               // 像调用add一样调用c
    
    f := add                          // 将函数add赋值给变量f了
    fmt.Printf("type of f:%T\n", f)   // type of f:func(int, int) int
    fmt.Println(f(10,20))             // 像调用add一样调用f
}

7、高阶函数

高阶函数分为函数作为参数和函数作为返回值两部分。

函数作为参数

函数可以作为参数:

func add(x, y int) int {
    return x + y
}
func calc(x, y int, op func(int, int) int) int {
    return op(x, y)
}
func main() {
    ret2 := calc(10, 20, add)
    fmt.Println(ret2) // 30
}

函数作为返回值

函数也可以作为返回值:

func do(s string) (func(int, int) int, error) {
    switch s{
        case "+":
        	return add, nil
        case "-":
        	return sub, nil
        default:
        err := errors.New("无法识别的操作符")
        return nil, err
    }
}

8、函数名和函数闭包

匿名函数

函数还可以作为返回值,但在Go语言中函数内部不能再像之前一样定义函数,只能定义匿名函数。匿名函数是没有函数名的函数,匿名函数定义格式:

func(参数)(返回值){
    函数体
}

匿名函数因为没有函数名,所以没有办法像普通函数一样进行调用,所以匿名函数需要保存到某个变量或者作为立即执行函数:

func main() {
    // 将匿名函数保存到变量
    add := func(x, y int) {
        fmt.Println(x + y)
    }
    add(10,20) // 通过函数定义完加()直接执行
    func(x, y int) {
        fmt.Println(x + y)
    }(10, 20)
}

匿名函数多用于实现回调函数和闭包。

闭包

闭包是一个函数和与其相关的引用环境组合而成的实体。简单理解:闭包=函数+引用环境

也可以这样理解:内函数引用了外函数参数,外函数返回了内函数。例:

func adder() func(int) int {
    var x int 
    return func(y int) int {
        c += y
        return x
    }
}
func main() {
    var f= addr()
    fmt.Println(f(10))  // 10
    fmt.Println(f(20))  // 30
    fmt.Println(f(30))  // 60
    
    f1 := adder()
    fmt.Println(f1(40))  // 40
    fmt.Println(f1(50))  // 90
}

变量f是一个函数并且他引用了其外部作用域中的x变量,此时f就是一个闭包。在f的生命周期内,变量x也一直有效。闭包进阶,

例1:

func adder2(x int) func(int) int {
    return func(y int) int {
        x += y 
        return x
    }
}
func main() {
    var f = adder2(10)
    fmt.Println(f(10))  // 20
    fmt.Println(f(20))  // 40
    fmt.Println(f(30))  // 70
    
    f1 := adder2(20)  
    fmt.Println(f1(40))  // 60
    fmt.Println(f1(50))  // 110
}

例2:

func makeSuffixFunc(suffix string) func(string) string {
	return func(name string) string {
		if !strings.HasSuffix(name, suffix) {
			return name + suffix
		}
		return name
	}
}

func main() {
	jpgFunc := makeSuffixFunc(".jpg")
	txtFunc := makeSuffixFunc(".txt")
	fmt.Println(jpgFunc("test")) //test.jpg
	fmt.Println(txtFunc("test")) //test.txt
}

例3:

func calc(base int) (func(int) int, func(int) int) {
	add := func(i int) int {
		base += i
		return base
	}

	sub := func(i int) int {
		base -= i
		return base
	}
	return add, sub
}

func main() {
	f1, f2 := calc(10)
	fmt.Println(f1(1), f2(2)) //11 9
	fmt.Println(f1(3), f2(4)) //12 8
	fmt.Println(f1(5), f2(6)) //13 7
}

9、defer语句

Go语言中的defer语句会将其后面跟随的语句进行延迟处理。在defer归属的函数即将返回时,将延迟处理的语句按defer定义的逆序进行执行,

先被defer的语句最后被执行,最后被defer的语句,最先被执行。

例:

func main() {
    fmt.Println(1)
    fmt.Println(2)
    fmt.Println(3)
    fmt.Println("end")
}

输出结果:

start
end
3
2
1

由于defer语句延迟调用的特性,所以defer语句能非常方便的处理资源释放问题。比如:资源清理、文件关闭、解锁及记录时间等。

defer执行时机

在Go语言的函数中return语句在底层并不是原子操作,它分为给返回值赋值和RET指令步。而defer语句执行的时机就在返回值赋值操作后,RET指令执行前。

第一步:返回值赋值
第二步:defer操作
第三步:return返回(RET指令)

10、内置函数介绍

内置函数 介绍
close 主要用来关闭channel
len 用来求长度,比如string、array、slice、map、channel
new 用来分配内存,主要用来分配值类型,比如int、struct。返回的是指针
make 用来分配内存,主要用来分配引用类型,比如channel、map、slice
append 用来追加元素到数组、slice中
panic和recover 用来做错误处理

panoc/recover

Go语言中目前(Go1.12)是没有异常机制,但是使用panic/recover模式来处理错误。panic可以放在任何地方引发,但recover只有在defer 调用的函数中有效。

例:

func funcA() {
    fmt.Println("func A")
}
func funcB() {
    panic("panic in B")
}
func funcC() {
    fmt.Println("func C")
}
func main() {
    funcA()
    funcB()
    funcC()
}

输出:

func A
panic: panic in B

goroutine 1 [running]:
main.funcB(...)
        .../code/func/main.go:12
main.main()
        .../code/func/main.go:20 +0x98

程序运行期间funcB中引发了panic导致程序崩溃,异常退出了。这个时候我们就可以通过recover将程序恢复回来,继续往后执行。

func funcA() {
	fmt.Println("func A")
}

func funcB() {
	defer func() {
		err := recover()
		//如果程序出出现了panic错误,可以通过recover恢复过来
		if err != nil {
			fmt.Println("recover in B")
		}
	}()
	panic("panic in B")
}

func funcC() {
	fmt.Println("func C")
}
func main() {
	funcA()
	funcB()
	funcC()
}

注意:

  1. recover()必须搭配defer使用。
  2. defer一定要在可能引发panic的语句之前定义。

十、指针

对于指针这个东西,学过C语言或C++语言的应该都了解,但是在Go中,指针的概念与C和C++中是不同的,Go语言中指针不能进行进行偏移和运算,是安全指针。

指针的三个基本概念:指针地址、指针类型和指针取值。

指针作用:指针是用来保存一个数据在内存中的地址的。

注:

  • Go语言中指针不能进行进行偏移和运算。

  • Go语言中的操作非常简单,只有两种形式两个符号

    • &取地址符:作用取地址
    • *****指针符:作用根据地址取值

1、指针地址和指针类型

每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go语言中使用&字符放在变量前面对变量进行“取地址操作”。Go语言中的值类型(int、float、bool、string、arrary、struct)都有对应的指针类型,如:*int*int64*string等。

取变量指针的语法:

ptr := &v  // v的类型为T

其中:

  • v:表示被取地址的变量,类型为T
  • ptr:表示接收地址的变量,ptr的类型为*T,称为T的指针类型。*代表指针。

示例:

func main() {
	a := 10
	b := &a
	fmt.Printf("a:%d ptr:%p\n", a, &a) // a:10 ptr:0xc00001a078
	fmt.Printf("b:%p type:%T\n", b, b) // b:0xc00001a078 type:*int
	fmt.Println(&b)                    // 0xc00000e018
}

2、指针取值

在对普通变量使用&操作符取地址后会获得这个变量的指针,然后可以对指针使用*操作,也就是指针取值,

示例:

func main() {
    // 指针取值
    a := 10
    b := &a  // 取变量a的地址,将指针保存到b中
    fmt.Printf("type of b:%T\n", b)
    c := *b  // 指针取值(根据指针去内存取值)
    fmt.Printf("type of c:%T\n", c)
    fmt.Printf("value of c:%v\n", c)
}

注:取地址操作符&和取值操作符是一对互补操作符,&取出地址,****根据地址取出地址指向的值。

变量、指针地址、指针变量、取地址、取值的相互关系和特性:

  • 对变量进行取地址(&)操作,可以获得变量的指针变量。
  • 指针变量的值是指针地址。
  • 对指针地址进行取值(*)操作,可以获得指针指向的原变量的值。

示例:

func modify1(x int) {
    x = 100
}

func modify2(x *int) {
    *x = 100
}

func main() {
    a := 10
    modify1(a)
    fmt.Println(a) // 10 只是把复制的值给改掉了,原值并没有变化
    modify1(&a)
    fmt.Println(a) // 100 原值变了是因为直接改变了地址的值,所传参数也是地址
}

3、new和make

示例:

func main() {
    var a *int
    *a = 100
    fmt.Println(*a)
    
    var b map[string]int
    b["asd"] = 100
    fmt.Print(b)
}

此代码会引发panic,在Go语言中对于引用类型的变量,在使用的时候不仅要声明,还要分配内存空间,否则值是没有办法进行存储的。对于值类型的声明不需要分配内存空间,因为他们在生命的时候已经默认分配好了内存空间。要分配内存,就涉及到new和make了。Go语言中new和make是内建的两个函数,主要用来分配内存。

panic原因:示例代码中*var a int 只是声明了一个指针变量a但是没有初始化,指针作为引用类型需要初始化后才能分配到内存空间,才可以进行赋值操作。正确写法示例:

func main() {
    var a * int
    a = new(int)
    *a = 10
    fmt.Println(*a)
}

示例中var b map[string]int只是声明变量b是一个map类型的变量,同理进行初始化址后才能进行赋值操作,示例:

func main() {
    var b map[string]int
    b = make(map[string]int, 10)
    b["asd"] = 100
    fmt.Println(b)
}

new

new是一个内置函数

函数签名:func new(Type) *Type

  • Type表示类型,new函数只接收一个参数,这个参数是一个类型
  • *Type表示类型指针,new函数返回一个指向该类的型内存地址的指针。

new函数不太常用,使用new函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值。

示例:

func main() {
    a := new(int)
    b := new(bool)
    fmt.Printf("%T\n", a) // *int
    fmt.Printf("%T\n", b) // *bool
    fmt.Printf(*a) // 0
    fmt.Printf(*b) // false
}

make

make也是用于内存分配的,区别于new,它只能用于slice、map、和channel的内存创建,它返回的也是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回指针了。

make函数的函数签名:

func make(t type, size ...IntegerType) Type

make函数是不能替代的,在使用slice、map以及channel的时候,都需要使用make进行初始化,然后才可以对他们进行操作。

new和make的区别

  1. 二者都是用来做内存分配的;
  2. make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身;
  3. 而new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针。

十一、结构体

Go语言中没有“类”这个概念,也不支持“类”的继承等面向对象的概念。Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。

1、类别名和自定义类型

自定义类型

在Go语言中有一些基本的数据类型,string、整形、浮点型、布尔型、复数,Go语言中可以使用type关键字来定义自定义数据类型。

自定义类型,是定义了一个新的数据类型。可以基于内置的基本类型定义,也可以通过struct定义

示例:

// 将MyInt定义为int类型
type MyInt int

通过type关键字定义,MyInt是一种新的类型,它具有int的特性。

类型别名

类型别名是Go1.9版本添加的新功能。

类型别名规定:TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型。

type TypeAlias = Type

rune和byte就是类型别名

type byte = unit8
type rune = int32

类型定义和类型别名的区别

类型别名与类型定义表面上只有一个等号的差异,结果显示a的类型是main.NewInt,表示main包下定义的NewInt类型。b的类型是intMyInt类型只会在代码中存在,编译完成时并不会有MyInt类型。

示例:

// 类型定义
type NewInt int

// 类型别名
type MyInt = int

func main() {
    var a NewInt
    var b MyInt
    
    fmt.Printf("type of a:%T\n", a) // type of a:main.NewInt
    fmt.Printf("type of b:%T\n", b) // type of b:int
}

2、结构体

Go语言中的基本数据类型可以表示一些事务的基本属性,但是当表达一个事物的全部或部分属性时,使用单一的基本数据类型是无法满足需求的,Go语言中提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫做结构体,英文名struct。通过struct可以定义自己的数据类型。

Go语言中通过struct实现面向对象。

2.1 结构体的定义

使用typestruct关键字来定义结构体

示例:

type 类型名 struct {
    字段名 字段类型
    字段名 字段类型
    ...
}
  • 类型名:标识自定义结构体的名称,在同一个包内不能重复。
  • 字段名:表示结构体字段名。结构体中的字段名必须唯一。
  • 字段类型:结构体字段的具体类型。

示例:

type person struct {
    name string
    city string
    age  int8
}

同样类型的字段可以写在一行

type person struct {
    name, city string
    age  int8
}

分析一下person这个结构体,它有三个字段,分别表示姓名、城市、和年龄。这就实现了和面向对象一样的效果。

语言内置的基础数据类型是用来描述一个值的,而结构体是用来描述一组值的。本质上是一种聚合型的数据类型

2.2 结构体实例化

只有当结构体实例化时,才会真正的分配内存。注:结构体必须实例化才能使用

结构体是值类型,可以像声明内置类型一样使用var关键字声明结构体类型。

var 结构体实例 结构体类型

基本实例化

示例:

type person struct {
    name string
    city string
    age  int8
}

func main() {
    var p1 person
    p1.name = "eric"
    p1.city = "北京"
    pi.age = 18
    fmt.Printf("p1=%v\n", p1) //p1={eric 北京 18}
    fmt.Printf("p1=%#v\n", p1) //p1=main.person{name:"eric", city:"北京", age:18}
}

可以通过.来访问结构体的字段(成员变量),例如p1.namep1.age

匿名结构体

在定义一些临时结构等场景下还可以使用匿名结构体。

package main

import (
	"fmt"
)

func () {
    var user struct{Name string; Age int}
    user.Name = "eric"
    user.Age = 18
    fmt.Printf("%#v\n", user)
}

创建指针类型结构体

可以使用new关键字对结构体进行实例化,得到的是结构体的地址。

var p2 = new(person)
fmt.Printf("%T\n", p2) //*main.person
fmt.Printf("p2=%#v\n", p2) //p2=&main.Person{name:"", city:"", ahe:0}

从打印的数据类型中显示p2是一个结构体指针。

Go语言中支持对结构体指针直接使用.访问结构体成员。

var p2 = new(person)
p2.name = "eric"
p2.age = 18
p2.city = "上海"
fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"eric", city:"上海", age: 18}

取结构体的地址实例化

使用&对结构体进行取地址操作相当于对该结构体类型进行了一次new实例化操作。

p3 := &person()
fmt.Printf("%T\n", p3) //*main.person
fmt.Printf("p3=%#v\n") // p3=&main.person{name:"", city:"", age:0}
p3.name = "eric"
p3.age = 18
p3.city = "成都"
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"eric", city:"成都", age:18}

p3.name="eric"这种写法是Go语言中的语法糖,标准写法是(*p3).name = "eric"

2.3 结构体初始化

未初始化的结构体,它的成员变量都是对应类型的零值。

type person struct {
    name string
    city string
    age int8
}

func main() {
    var p4 person
    fmt.Printf("p4=%#v\n", p4) //p4=main.person{name:"", city:"", age:0}
}

使用键值对方式初始化

使用键值对对话结构体进行初始化时,见对应结构体的字段,值对应该字段的初始值。

结构体键值对结构体初始化示例:

p5 := person{
    name: "eric",
    city: "北京",
    age: 18,
}
fmt.Printf("p5=%#v\n", p5) //p5=main.person{name:"eric", city:"北京", age:18}

结构体指针键值对初始化示例:

p6 := &person{
    name: "eric",
    city: "成都",
    age: 18,
}
fmt.Printf("p6=%#v\n", p6) //p6=&main.person{name:"eric", city:"成都", age: 18}

注:当某些字段没有初始值的时候,该字段可以不写,没有指定的初始值的字段的值就是该字段类型的零值。

p7 := &person{
    city: "成都",
}
fmt.Print("p7=%#v\n", p7) //p7=&main.person{name:"", city:"成都", age:0}

使用值的方式初始化

初始化结构体有简写方法,在初始化的时候不写键,直接写值

示例:

p8 := &person{
    "eric",
    "成都",
    18,
}
fmt.Printf("p8=%#v\n", p8) //p8=&main.person{name:"eric", city:"成都", age:18}

注:

  1. 必须初始化结构体的所有字段。
  2. 初始值的填充顺序必须与字段在结构体中的声明顺序一致。
  3. 此方式不能和键值初始化方式混用。

2.4 结构体内存布局

结构体占用一块连续的内存

package main

import "fmt"

func main() {
	type test struct {
		a int8
		b int8
		c int8
		d int8
	}
	n := test{
		1, 2, 3, 4,
	}
	fmt.Printf("n.a %p\n", &n.a)
	fmt.Printf("n.b %p\n", &n.b)
	fmt.Printf("n.c %p\n", &n.c)
	fmt.Printf("n.d %p\n", &n.d)
}

结果:

n.a 0xc0000a2058
n.b 0xc0000a2059
n.c 0xc0000a205a
n.d 0xc0000a205b

里面的a和b是指10和11,0x代表是16进制

空结构体

空结构体不占用内存空间

var v struct{}
fmt.Print(unsafe.Sizeof(v)) //0

type person struct{}
var p person
fmt.Print(unsafe.Sizeof(p)) //0

type person struct{}
p := person{}
fmt.Print(unsafe.Sizeof(p)) //0

3、构造函数

Go语言的结构体没有构造函数,但可以实现。

示例:

func newPerson(name, city string, age int8) *person {
    return &person{
        name: name,
        city: city,
        age:  age,
    }
}

调用构造函数示例:

p9 := newPerson("eric", "成都", 18)
fmt.Printf("%#v\n", p9) //&main.person{name:"eric", "成都", age:18}

4、方法和接收者

Go语言中的方法(Method)是一种用于特定类型变量的函数。这种特定类型变量叫接收者(Receiver)。接收者的概念类似于其它语言中的this或者self

定义方式:

func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
    函数体
}
  • 接收者变量:接收者中的参数变量在命名时,官方建议使用接收者类型名称首字母的小写,而不是selfthis等命名。例:Dog 命名为 d
  • 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
  • 方法名、参数列表、返回参数:具体格式与函数定义相同。

示例:

//Person 结构体
type Person struct {
    name string
    age  int8
}

//NewPerson 构造函数
func NewPerson(name string, age int8) *Person {
    return &Person{
        name: name,
        age:  age,
    }
}

//Dream Person做梦方法
func (p Person) Dream() {
    fmt.Printf("%s的梦想是学好Go语言!\n", p.name)
}

func main() {
    p1 := NewPerson("eric", 18)
    p1.Dream()
}

注:方法与函数的区别是,函数不属于任何类型,方法属于特定类型。

值类型的接收者

当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。在类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身。

//SetAge 设置p的年龄
//使用值接收者
func (p Person) SetAge2(newAge int8) {
	p.age = newAge
}

func main() {
	p1 := NewPerson("eric", 18)
	p1.Dream()
	fmt.Println(p1.age) // 18
	p1.SetAge2(20) // (*p1).SetAge2(20)
	fmt.Println(p1.age) // 18
}

指针类型的接收者

指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,也就是直接修改地址中对应的值。这种方式类似于其它语言的thisself

//SetAge 设置p的年龄
//使用指针接收者
func (p *Person) SetAge(newAge int8) {
    p.age = newAge
}

调用方法示例:

func main() {
    p1 := NewPerson("eric", 18)
    fmt.Println(p1.age) // 18
    p1.SetAge(20)
    fmt.Println(p1.age)
}

指针类型接收者的使用场景

  1. 需要修改接收者中的值
  2. 接收者是拷贝代价比较大的大对象
  3. 保证一致性,如果有某个方法使用了指针接收者,那么其它的方法也应该使用指针接收者。

5、任意类型添加方法

在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。

示例:

//MyInt 将int定义为自定义MyInt类型
type MyInt int

//SayHello 为MyInt添加一个SayHello方法
func (m MyInt) SayHello() {
    fmt.Println("Hello, 我是一个int。")
}

func main() {
    var m1 MyInt
    m1.SayHello() //Hello, 我是一个int。
    m1 = 100
    fmt.Printf("%#v  %T\n", m1, m1) //100 main.MyInt
}

注:非本地类型不能定义方法,也就是说不能给别的包的类型定义方法。

6、结构体的匿名字段

结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段。

//Person 结构体Perdson类型
type Person struct {
    string
    int
}

func main() {
    p1 := Person{
        "eric",
        18,
    }
    fmt.Printf("%#v\n", p1)        //main.Person{string:"北京", int:18}
	fmt.Println(p1.string, p1.int) //北京 18
}

注:这里匿名字段的说法并不代表没有字段名,而是默认会采用类型名作为字段名,结构体要求字段名必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。

7、嵌套结构体

一个结构体中可以嵌套包含另一个结构体或结构体指针。

示例:

//Address 地址结构体
type Address struct {
    Province string
    City     string
}

//User 用户结构体
type User struct {
    Name    string
    Gender  string
    Address Address
}

func main() {
    user1 := User{
        Name:   "eric",
        Gender: "男",
        Address: Address{
            Province: "成都",
            City:     "简阳",
        },
    }
    fmt.Printf("user1=%#v\n", user1) //user1=main.User{Name:"eric", Gender:"男", Address:main.Address{Province:"成都", City:"简阳"}}
}

嵌套匿名字段

嵌套的Address结构体也可以采用匿名字段的方式

示例:

//Address 地址结构体
type Address struct {
    Province string
    City     string
}

//User 用户结构体
type User struct {
    Name   string
    Gender string
    Address //匿名字段
}

func main() {
    var user2 User
    user2.Name = "eric"
    user2.Gender = "男"
    user2.Address.Province = "成都" //匿名字段默认使用类型名作为字段名
    user2.City = "简阳"             //匿名字段可以省略
    fmt.Printf("user2=%#v\n", user2) //user2=main.User{Name:"eric", Gender:"男", Address:main.Address{Province:"成都", City:"简阳"}}
}

当访问结构体成员时会先在结构体中查找该字段,找不到再去嵌套的匿名字段中查找。

嵌套结构体的字段名冲突

嵌套结构体内部可能存在相同的字段名。在这种情况下为了避免歧义需要通过指定具体的内嵌结构体字段名。

//Address 地址结构体
type Address struct {
    Province   string
    City       string
    CreateTime string
}

//Email 邮箱结构体
type Email struct {
    Account    string
    CreateTime string
}

//User 用户结构体
type User struct {
    Name   string
    Gender string
    Address
    Email
}

func main() {
    var user3 User
    user3.Name = "eric"
    user3.Gender = "男"
    // user3.CreateTime = "2019" //ambigous selector user3.CreateTime
    user3.Address.CreateTime = "2020" // 指定Address结构体中的CreateTime
    user3.Email.CreateTime = "2020" // 指定Email结构体中的CreateTime
}

8、结构体的“继承”

Go语言中使用结构体可以实现面向对象中的继承。

示例:

//Animal 动物
type Animal struct {
    name string
}

func (a *Animal) move() {
    fmt.Printf("%s会动!\n", a.name)
}

//Dog 狗
type Dog struct{
    Feet    int8
    *Animal // 通过嵌套匿名结构体实现继承
}

func (d Dog) wang() {
    fmt.Printf("%s会汪汪~\n", d.name)
}

func main() {
    d1 := &Dog{
        Feet: 4,
        Animal: &Animal{ //注意嵌套的是结构体指针
            name: "eric",
        }
    }
    d1.wang() //eric会汪汪~
    d1.move() //eric会动!
}

9、结构体字段的可见性

结构体中字段大写开头表示可公开访问,小写表示私有(仅定义当前结构体的包中可访问)。

10、结构体与JSON序列化

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式。易于阅读和编写。同时也易于机器解析和生成。JSON键值对是用来保存JS对象的一种方式,键/值对组合中的键名写在前面并用双引号""包裹,使用:分隔,然后紧接着值,多个键值之间使用英文分隔。

//Student 学生
type Student struct {
    ID     int
    Gender string
    Name   string
}

//Class 班级
type Class struct {
    Title    string
    Students []*Student
}

func main() {
    c := &Class{
        Title:    "101",
        Students: make([]*Student, 0, 200),
    }
    for i := 0; i < 10; i++ {
        stu := &Student{
            Name:   fmt.Sprintf("stu%02d", i),
            Gender: "男",
            ID:     i,
        }
        c.Students = append(c.Students, stu)
    }
    //JSON序列化:结构体-->JSON格式的字符串
    data, err := json.Marshal(c)
    if err !=nil {
        fmt.Println("json marshal failed")
        return
    }
    fmt.Print("json:%s\n", data)
    //JSON反序列化:JSON格式的字符串-->结构体
    str := `{"Title":"101","Students":[{"ID":0,"Gender":"男","Name":"stu00"},{"ID":1,"Gender":"男","Name":"stu01"},{"ID":2,"Gender":"男","Name":"stu02"},{"ID":3,"Gender":"男","Name":"stu03"},{"ID":4,"Gender":"男","Name":"stu04"},{"ID":5,"Gender":"男","Name":"stu05"},{"ID":6,"Gender":"男","Name":"stu06"},{"ID":7,"Gender":"男","Name":"stu07"},{"ID":8,"Gender":"男","Name":"stu08"},{"ID":9,"Gender":"男","Name":"stu09"}]}`
    c1 := &Class{}
    err = json.Unmarshal([]byte(str), c1)
    if err != nil {
        fmt.Println("json unmarshal failed!")
        return
    }
    fmt.Printf("%#v\n", c1)
}

11、结构体标签(Tag)

Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来。Tag在结构体字段的后方定义,由一对反引号包裹起来,具体格式:

`key1:"value1" key2:"value2"`

结构体tag由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。同一个结构体字段可以设置多个键值对tag,不同的键值对之间使用空格分隔。

注:为结构体编写Tag时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如:不要在key和value之间加空格。

例如我们为Student结构体的每个字段定义json序列化时使用的Tag:

//Student 学生
type Student struct {
	ID     int    `json:"id"` //通过指定tag实现json序列化该字段时的key
	Gender string //json序列化是默认使用字段名作为key
	name   string //私有不能被json包访问
}

func main() {
	s1 := Student{
		ID:     1,
		Gender: "男",
		name:   "eric",
	}
	data, err := json.Marshal(s1)
	if err != nil {
		fmt.Println("json marshal failed!")
		return
	}
	fmt.Printf("json str:%s\n", data) //json str:{"id":1,"Gender":"男"}
}

12、结构体和方法补充知识点

因为slice和map这两种数据类型都包含了指向底层数据的指针,因此我们在需要复制它们时要特别注意。

示例:

type Person struct {
	name   string
	age    int8
	dreams []string
}

func (p *Person) SetDreams(dreams []string) {
	p.dreams = dreams
}

func main() {
	p1 := Person{name: "eric", age: 18}
	data := []string{"吃饭", "睡觉", "打豆豆"}
	p1.SetDreams(data)

	// 你真的想要修改 p1.dreams 吗?
	data[1] = "不睡觉"
	fmt.Println(p1.dreams)  // ?
}

正确的做法是在方法中使用传入的slice的拷贝进行结构体赋值。

func (p *Person) SetDreams(dreams []string) {
	p.dreams = make([]string, len(dreams))
	copy(p.dreams, dreams)
}

同样的问题也存在于返回值slice和map的情况,在实际编码过程中一定要注意这个问题。

十二、接口

接口(interface)定义了一个对象的行为规范,只定义规范不实现,具体的对象来实现规范性的细节。

1、接口类型

在Go语言中接口(interface)是一种类型,一种抽象的类型。

interface是一组method的集合,是duck-type programming的一种体现。接口做的事就是定义一个协议(规则),并不关心属性(数据),只关心行为(方法).

注:接口(interface)是一种类型。

接口的概念:接口是一组方法签名的集合,可以通过定义一个结构体实现该接口的所有方法。(接口定义了对象的行为)

在Go语言中没有明确提到一个类型是否实现了一个接口,如果一个类型实现了在接口中定义的签名方法,则称该类型实现了该接口

2、接口的定义

Go语言提倡面向接口编程。

每个接口由数个方法组成,接口定义格式:

type 接口类型名 interface{
    方法名1(参数列表1) 返回值列表1
    方法名2(参数列表2) 返回值列表2
    ...
}
  • 接口名:使用type将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫writer,有字符串功能的接口叫Stringer等。接口名最好突出该接口定义的类型含义。
  • 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
  • 参数列表、返回值列表:参数列表和返回值列表中的参数变量可以省略。

示例:

type Person interface{
    eat([]byte) error
}

3、实现接口的条件

一个对象只要全部实现了接口中的方法,那么就实现了这个接口。换而言之:接口就是一个需要实现的方法列表

定义一个接口:

// Sayer 接口
type Sayer interface {
    say()
}

定义dogcat两个结构体:

type dog struct {}
type cat struct {}

因为Sayer接口里只有一个say方法,所以下只需要给dogcat分别实现say方法就可以实现Sayer接口了。

// dog实现了Sayer接口
func (dog) say() {
    fmt.Println("汪汪汪")
}

// cat实现Sayer接口
func(c cat) asy() {
    fmt.Println("喵喵喵")
}

接口的实现:只要实现了接口中的所有方法就实现了这个接口

4、接口类型变量

接口类型变量能够存储实现了该接口的实例,上面的Sayer类型的变量能够存储dogcat类型的变量。

func () {
    var x Sayer // 声明一个Sayer类型的变量x
    a := cat()  // 实例化一个cat
    b := dog()  // 实例化一个dog
    x = a       // 可以把cat实例直接赋值给x
    x.say()     // 喵喵喵
    x = b       // 可以把dog实例直接赋值给x
    x.say()     // 汪汪汪
}

Tips:_的使用

// 摘自gin框架routergroup.go
type IRouter interface {...}

type RouterGroup struct {...}

var _IRouter = &RouterGroup{} // 确保RouterGroup实现了接口IRouter

5、值接收者和指针接收者实现接口的区别

使用值接收者实现接口和使用指针接收者实现接口的区别:

示例:Moverdog两个结构体

type Mover interfance {
    move()
}

type dog struct{}

值接收者实现接口

func(d dog) move() {
    fmt.Println("狗会动")
}

此时实现接口的是dog类型:

func main() {
    var x Mover
    var wangcai = dog{} // 旺财是dog类型
    x = wangcai         // x可以接收dog类型
    var fugui = &dog{}  // 富贵是*dog类型
    x = fugui           // x可以接收*dog类型
    x.move()
}

使用值接收者实现接口后,不管是dog结构体还是结构体指针*dog类型的变量都可以赋值给接口变量。因为Go语言中有针对指针类型变量求值的语法糖,dog指针fugui内部会自动求值*fugui

指针接收者实现接口

示例:

func (d *dog) move() {
    fmt.Println("狗会动")
}
func main() {
    var x Mover
    var wangcai = dog{} // 旺财是dog类型
    x = wangcai         // x不可以接收dog类型
    var fugui = &dog{}  // 富贵是*dog类型
    x = fugui           // x可以接收*dog类型
}

此时实现Mover的接口是dog类型,所以不能给x传入dog类型的wangcai,此时x只能存储*dog类型的值。

6、类型与接口的关系

一个类型实现多个接口

一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。

示例:

// Sayer 接口
type Sayer interfance {
    say()
}

// Mover 接口
type Mover interfance {
    move()
}

创建一个dog结构体,既可以实现Sayer接口,也可以实现Mover接口。

type dog struct {
    name string
}

// 实现Sayer接口
func (d dog) say() {
    fmt.Printf("%s会汪汪叫\n",d.name)
}

// 实现Mover接口
func (d dog) move() {
    fmt.Printf("%s会动\n", d.name)
}

func main() {
    var x Sayer
    var y Mover
    
    var a = dog{name:"旺财"}
    x = a
    y = a
    x.say()
    y.move()
}

多个类型实现同一接口

Go语言中不同的类型还可以实现同一接口。

示例:

// Mover 接口
type Mover interfance {
    move()
}

创建两个结构体dogcar,都实现Mover接口

type dog struct {
    name string
}
 
type car struct {
    brand string
}

// dog类型实现Mover接口
func(d dog) move() {
    fmt.Printf("%s会跑\n", d.name)
}

// car类型实现Mover接口
func (c car) move() {
    fmt.Printf("%s速度70迈\n", c.brand)
}

这个时候就可以忽略他们具体是什么,只需要调用他们的move方法就可以。

func main() {
    var x Mover
    var a = dog{name: "旺财"}
    var b = car{brand: "保时捷"}
    x = a
    x.move()
    x = b
    x.move()
}

注:一个接口的方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。

示例:

// WashingMachine 洗衣机
type WashingMachine interfance {
    wash()
    dry()
}

// 甩干器
type dryer struct{}

// 实现WsahingMachine接口的dry()方法
func (d dryer) dry() {
    fmt.Println("甩一甩")
}

// 海尔洗衣机
type haier struct {
    dryer //嵌入甩干器
}

// 实现WashingMachine接口的wash()方法
func (h haier) wash() {
    fmt.Println("洗刷刷")
}

7、接口嵌套

接口与接口间可以通过嵌套创造出新的接口。

// Sayer 接口
type Sayer interfance {
    say()
}

// Mover 接口
type Mober interfance {
    move()
}

// 接口嵌套
type animal interfance {
    Sayer
    Mover
}

嵌套得到的接口的使用与普通接口一样:

type cat struct {
    name string
}

func (c cat) say() {
    fmt.Println("喵喵喵")
}

func (c cat) move() {
    fmt.Println("猫会动")
}

func main() {
    var x animal
    x = cat(name: "花花")
    x.move()
    x.say()
}

8、空接口

空接口的定义

空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。

空接口类型的变量可以存储任意类型的变量。

func mian() {
    // 定义一个空接口x
    var x = interfance{}
    s := "Hello 沙河"
    x = s 
    fmt.Printf("type:%T value:%v\n", x, x)
    i := 100
    x = i
    fmt.Print("type:%T value:%v\n", x, x)
    b := true
    x = b
    fmt.Printf("type:%T value:%v\n", x, x)
}

接口的应用

空接口作为函数的参数

使用空接口实现可以接收任意类型的的函数参数。

// 空接口作为函数参数
func show(a interfance{}) {
    fmt.Printf("type:%T value:%v\n", a, a)
}

空接口作为map的值

使用空接口实现可以保存任意值的字典。

// 空接口作为map值
var studentInfo = make(map[string]interfance{})
studentInfo["name"] = "Eric"
studentInfo["age"] = 18
studentInfo["married"] = false
fmt.Println(studentInfo)

9、类型断言

空接口可以存储任意类型的值,获取空接口中存储的具体数据。

接口值

一个接口的值(简称接口值)是由一个具体类型具体类型的值两部分组成。这两部分称为接口的动态类型动态值

示例:

var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil

判断空接口中的值使用断言:

断言语法格式:

x.(T)
  • x:表示类型为interface{}的变量。
  • T:表示断言x可能是的类型。

改语法返回两个参数,第一个参数是x转化为T类型后的变量,第二个值是一个布尔值,若为true则表示断言成功,为false在表示断言失败。

示例:

func main() {
    var x interfance{}
    x = "Hello 沙河"
    v, ok := x.(string)
    if ok {
        fmt.Println(v)
    } else {
        fmt.Println("类型断言失败")
    }
}

如果需要断言多次就需要写多个if判断, 这时可以使用switch语句来实现:

func justifyType(x interfance{}) {
    switch v:= x.(type) {
    case string:
        fmt.Printf("x is a string,value is %v\n", v)
    case int:
        fmt.Printf("x is a int is %v\n", v)
    case bool:
        fmt.Printf("x is a bool is %v\n", v)
    default:
        fmt.Printf("unsupport type!")
    }
}

空接口有可以存储任意类型值的特点,所以空接口在Go语言中的使用十分广泛。

注意:只有当两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。不要为了接口而写接口,那样只会增加不必要的抽象,导致不必要的运行时损耗。

十三、包

在工程化的Go语言开发项目中,Go语言的源码复用是建立在包(package)基础之上的。

1、Go语言的包介绍

包(package)是多个Go源码的集合,是一种高级的代码复用方案,Go语言为我们提供了很多内志保,如fmtosio等。

2、定义包

我们可以根据自己需要创建自己的包。一个包可以简单理解为一个存放.go文件的文件夹。该文件夹下面的所有go文件都要在代码的第一行添加如下代码,声明该文件归属的包。

package 包名

注意:

  • 一个文件夹下面直接包含的文件只能归属一个package,同样一个package的文件不能在多个文件夹下
  • 报名可以不和文件夹的名字一样,包名不能包含 - 符号。
  • 包名为main的包为应用程序的入口包,这种包编译后会得到一个可执行文件,而编译不包含main包的源代码则不会得到可执行文件。

3、可见性

如果想在一个包中引用另外一个包里的标识符(如变量、常量、类型、函数等)时,该标识符必须是对外可见的(pubilc)。在Go语言中只需要将标识符的首字母大写就可以让标识符对外可见了。

示例:

package pkg2

import "fmt"

// 包变量的可见性
var a = 100 // 首字母小写,外部包不可见,只能在当前包内使用

// 首字母大写外部包可见,可在其他包中使用
const Mode = 1

type person struct { // 首字母小写,外部包不可见,只能在当前包内使用
    name string
}

// 首字母大写,外部包可见,可在其他包中使用
func Add(x, y int) int {
    return x + y
}

func age() { // 首字母小写,外部包不可见,只能在当前包内使用
    var Age = 18 // 函数局部变量,外部包不可见,只能当函数内使用
    fmt.Println(Age)
}

结构体中的字段名和接口中的方法名如果首字母都是大写,外部包可以访问这些字段和方法。

示例:

type nStudent struct {
    Name  string //可在包外访问的方法
    class string //仅限包内访问
}

type Payer interfance {
    init() //仅限包内访问的方法
    Pay()  //可在包外访问的方法
}

4、包的导入

要在代码中引入其他包的内容,需要使用import关键字导入使用的包。

具体语法:

import ”包的路径“

注意:

  • import导入语句通常放在文件开头包声明的语句下面。
  • 导入的包名需要使用双引号包裹起来。
  • 包明是从$GOPATH/src/后开始计算的,使用/进行路径分隔。
  • Go语言中禁止循环导入包。

单行导入

单行导入包的格式:

import "package1"
import "package2"

多行导入

多行导入包的格式:

import (
    "package1"
    "package2"
)

5、自定义包名

在导入报名的时候,我们还可以为导入的包设置别名。通常用于导入的包名太长或者导入的包名冲突的情况。

具体语法:

import 别名 "包的路径"

单行导入方式定义别名:

import "fmt"
import m "github.com/pcd/studygo/pkg_test"

func main() {
    fmt.Println(m.Add(100, 200))
    fmt.Println(m.Mode)
}

多行导入方式定义别名:

import (
    "fmt"
    m "github.com/pcd/studygo/pkg_test"
)

func main() {
    fmt.Println(m.Add(100,200))
    fmt.Println(m.Mode)
}

6、匿名导入包

如果只希望导入包,而不使用包内部的数据时,可以使用匿名导入包。

使用格式:

import _ "包的路径"

匿名导入的包与其他方式导入的包一样都会被编译到可执行文件中

7、init()初始化函数

init()函数介绍

在Go语言程序执行时导入包语句会自动触发包内部init()函数的调用。需要注意的是:init()函数没有参数也没有返回值。init()函数在程序运行时自动被调用执行,不能在代码中主动调用它。

init()函数执行顺序

Go语言包会从main包开始检查其导入的所有包,每个包中又可能导入了其它的包。

Go编译器由此构建出一个树状的包引用关系,再根据引用顺序决定编译顺序,依次编译这些包的代码。

在运行时,被最后导入的包会最先初始化并调用其init()函数。

十四、Go语言常用标准库

  • fmt标准库中的一些常用函数。
  • fmt包实现了类似C语言的printf和scanf的格式化I/O。只要分为向外输出内容和获取内容两大部分。

1、fmt向外输出

标准库fmt提供了以下几种输出相关函数。

Print

Print系列函数会将内容输出到系统的标准输出,区别在于Print函数直接输出内容,printf函数支持格式化输出字符串,Println函数会在输出内容的结尾添加一个换行符。

func Print(a ...interface{}) (n int, err error)
func Printf(format string, a ...interface{}) (n int, err error)
func Println(a ...interfance{}) (n int , err error)

示例:

func main() {
    fmt.Print("在终端打印该信息。")
    name := "Eric"
    fmt.Printf("我是:%s\n", name)
    fmt.Println("在终端打印单独一行显示")
}

输出示例:

在终端打印该信息。我是:Eric
在终端打印单独一行显示。

Fprint

Fprint系列函数会将内容输出到一个io.Writer接口类型的变量w中,我们通常使用这个函数往文件中写入内容。

func Fprint(w io.Writer, a ...interfance{}) (n int, err error)
func Fprintf(w io.Writer, format string, a ...interfance{}) (n int, err error)
func Fprintln(w io.Writer, a ...interfance{}) (n int, err error)

示例:

// 向标准输出写入内容
fmt.Fprint(os.Stdout, "向标准输出写入内容")
fileObj, err := os.OpenFile("./xxx.txt", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
    fmt.Println("打开文件出错,err:", err)
    return
}
name := "Eric"
// 向打开的文件句柄中写入内容
fmt.Fprintf(fileObj, "往文件中写入信息:%s", name)

注意:只要满足io.Writer接口的类型都支持写入。

Sprint

Sprint系列函数会把传入的数据生成并返回一个字符串。

func Sprint(a ...interfance{}) string
func sprintf(format string, a ...interfance{}) string
func Sprintln(a ...interface{}) string

示例:

s1 := fmt.Sprint("Eric")
name := "Eric"
age := 18
s2 := fmt.Sprintf("name:%s, age:%d", name, age)
s3 := fmt.Sprintfln("Eric")
fmt.Println(s1, s2, s3)

Errorf

Errorf函数根据format参数生成格式化字符串并返回一个包含该字符串的错误。

func Errorf(format string, a ...interfance{}) error

通常使用这种方式类自定义错误类型,例如:

err := fmt.Errorf("这是一个错误")

Go1.13版本为fmt.Errorf函数增加了一个%w占位符用来生成一个可以包裹Error的Wrapping Error。

e := errors.New("原始错误e")
w := fmt.Errorf("Wrap了一个错误%w", e)

2、格式化占位符

*printf系列函数都支持format格式化参数,在这里我们按照占位符将被替换的变量类型划分,方便查询和记忆。

通用占位符

占位符 说明
%v 值的默认格式表示
%+v 类似%v,但输出结构体时会添加字段名。
%#v 值的Go语法表示
%T 打印值的类型
%% 百分号

示例:

fmt.Printf("%v\n", 100)
fmt.Printf("%v\n", false)
o := struct{ name string }{"Eric"}
fmt.Printf("%v\n", o)
fmt.Printf("%#v\n", o)
fmt.Printf("%T\n", o)
fmt.Printf(100%%\n)

输出:

100
false
{Eric}
struct { name string }{ name:"Eric" }
struct { name string }
100%

布尔型

占位符 说明
%t true或false

整形

占位符 说明
%b 表示为二进制
%c 该值对应的unicode
%d 表示为十进制
%o 表示为八进制
%x 表示为十六进制,使用a-f
%X 表示为十六进制,使用A-F
%U 表示为Unicode格式:U+1234,等价于“U+%04X”
%q 该值对应的单引号括起来的Go语法字符字面值,必要时会采用安全的转义表示

示例:

n := 65
fmt.Printf("%b\n", n)
fmt.Printf("%c\n", n)
fmt.Printf("%d\n", n)
fmt.printf("%o\n", n)
fmt.Printf("%x\n", n)
fmt.Printf("%X\n", n)

输出:

1000001
A
65
101
41
41

浮点数与复数

占位符 说明
%b 无小数部分、二进制指数的科学计数法,如-123456p-78
%e 科学计数法,如-1234.656e+78
%E 科学计数法,如-1234.456E+78
%f 有小数部分但无指数部分,如123.456
%F 等价于%f
%g 根据实际情况采用%e或者%f格式(以获得更简洁、准确的输出)
%G 根据实际情况采用%E或者%F格式(以获得更简洁、准确的输出)

示例:

f := 12.34
fmt.Printf("%b\n", f)
fmt.Printf("%e\n", f)
fmt.Printf("%E\n", f)
fmt.Printf("%f\n", f)
fmt.Printf("%g\n", f)
fmt.Printf("%G\n", f)

输出:

6946802425218990p-49
1.2340004+01
1.234000E+01
12.340000
12.34
12.34

字符串和[]byte

占位符 说明
%s 直接输出字符串或者[]byte
%q 该值对应的双引号括起来的go语法字符串字面值,必要时会采用安全的转义表示
%x 每个字节用两个字符十六进制数表示(使用a-f)
%X 每个字节用两个字符十六进制数表示(使用A-F)

示例:

s := "Eric"
fmt.Printf("%s\n", s)
fmt.Printf("%q\n", s)
fmt.Printf("%x\n", s)
fmt.Printf("%X\n", s)

输出:

Eric
"Eric"
e5b08fe78e8be5ad90
E5B08FE78E8BE5AD90

指针

占位符 说名
%p 表示为十六进制,并加上前导的ox

示例:

a := 10
fmt.Printf("%p\n", &a)
fmt.Printf("%#p\n", &a)

输出:

0xc000094000
c000094000

宽度标识符

宽度标识符通过一个紧跟在百分号后面的十进制数指定,如果未指定宽度,表示值时除必须之外不做填充。精度通过(可选的)宽度后跟点号后跟的十进制数指定。如果未指定精度,会使用默认精度;如果点好后没有跟数字,表示精度为0.

占位符 说明
%f 默认宽度,默认精度
%9f 宽度9,默认精度
%.2f 默认宽度,精度2
%9.2f 宽度9,精度2
%9.f 宽度9,精度0

示例:

n := 12.34
fmt.Printf("%f\n", n)
fmt.Printf("%9f\n", n)
fmt.Printf("%.2f\n", n)
fmt.Printf("%9.2f\n", n)
fmt.printf("%9.f\n", n)

输出:

12.340000
12.340000
12.34
    12.34
       12

其他falg

占位符 说明
'+' 总是输出数值的正负号;对%q(%+q)会生成全部是ASCII字符的输出(通过转义);
’ ‘ 对数值,正数前加空格而负数前加负号;对字符串采用%x或%X时(%x或%X)会给各打印的字节之间加空格
'_' 在输出右边填充空白而不是默认的左边(即从默认的右对齐切换为左对齐)
'#' 八进制数前加0(%#0),十六进制数前加0x(%#x)或0X(%#X),指针去掉前面的0x(%#p)对%q(%#q),对%U(%#U)会输出空格和单引号扩起来的go字面值
'0' 使用0而不是空格填充,对于数值类型会把填充的0放在正负号后面

示例:

s := "Eric"
fmt.Printf("%s\n", s)
fmt.Printf("%5s\n", s)
fmt.Printf("%-5s\n", s)
fmt.Printf("%5.7s\n", s)
fmt.Printf("%5.2s\n", s)
fmt.Printf("%05s\n", s)

输出:

Eric
  Eric
Eric  
  Eric
Eric  
   Eric
00Eric

3、获取输入

Go语言fmt包下有fmt.Scanfmt.Scanffmt.Scanln三个函数,可以在程序运行过程中从标准输入获取用户的输入。

fmt.Scan

函数定签名示例:

func Scan(a ...interfance{}) (n int, error)
  • Scan从标准库输入扫描文本,读取由空白符分隔的值保存到传递给本函数的参数中,换行符视为空白符。
  • 本函数返回成功扫描的数据个数和遇到的任何错误。如果读取的数据个数比提供的参数少,会返回一个错误报告原因。

示例:

func main() {
    var (
        name    string
        age     int
        married bool
    )
    fmt.Scan(&name, &age, &married)
    fmt.Printf("扫描结果 name:%s age:%d married:%t \n", name, age, married)
}

fmt.Scan从标准输入中扫描用户输入的数据,将以·空白符分隔的数据分别存入指定的参数。

fmt.Scanf

函数签名示例:

func Scanf(fomart styring, a ...interfance{}) (n int, err error)
  • Scanf从标准输入扫描文本,根据format参数指定的格式去读取由空白符分隔的值保存到传递给本函数的参数中。
  • 本函数返回成功扫描的数据和遇到的任何错误。

示例:

func main() {
    var {
        name    string
        age     int
        married bool
    }
    fmt.Scanf("1:%s 2:%d 3:%t", &name, &age, &married)
    fmt.Printf("扫描结果 name:%s age:%d married:%t \n", name, age, married)
}

fmt.Scanf不同于fmt.Scan简单的以空格作为输入数据的分隔符,fmt.Scanf为输入数据指定了具体的输入内容格式,只有按照格式输入数据才会被扫描并存入对应的变量。

fmt.Scanln

函数签名示例:

func Scanln(a ...interfance{}) (n int, err error)
  • Scanln类似Scan,它在遇到换行时才停止扫描。最后一个数据后面必须有换行或者到达结束位置。
  • 本函数返回成功扫描的数据个数和遇到的任何错误。

示例:

func main() {
    var {
        name    string
        age     int
        married bool
    }
    fmt.Scanln(&name, &age, &married)
    fmt.Printf("扫描结果 name:%s age:%d married:%t \n", name, age, married)
}

fmt.Scanln遇到回车就结束扫描了,这个比较常用

bufio.NewReader

有时候我们需要完整获取输入的内容,而输入的内容可能包含空格,这种情况下可以使用bufio包来实现。

示例:

func bufioDemo() {
    reader := bufio.NewReader(os.Stdio) // 从标准输入对象
    fmt.Print("请输入内容:")
    text, _:= reader.ReadString('\n') // 读到换行
    text = strings.TrimSpace(text)
    fmt.Printf("%#v\n", text)
}

Fscan系列

这几个函数功能分别类似于fmt.Scanfmt.Scanffmt.Scanln三个函数,只不过他们不是从标准输入中读取数据而是从io.Reader中读取数据。

func Fscan(r io.Reader, a ...interfance{}) (n int, err error)
func Fscanln(r io.Reader, a ...interfance{}) (n int, err error)
func Fscanf(r io.Readerm fromat string, a ...interfance{}) (n int, err error)

Scan系列

这几个函数功能分别类似于fmt.Scanfmt.Scanffmt.Scanln三个函数,只不过他们不是从标准输入中读取数据而是从指定字符串中读取数据。

func Sscan(str string, a ...interfance{}) (n int, err error)
func Sscanln(str string, a ...interfance{}) (n int, err error)
func Sscanf(str string, format string, a ...interface{}) (n int, err error)

十五、Time包

time包提供了时间的显示和测量用的函数。日历的计算采用的是公历。

1、时间类型

time.Time类型表示时间。我们可以通过time.Now()函数获取当前的时间对象,然后获取时间对象的年月日时分秒等信息。

示例:

func timeDemo() {
    now := time.Now() //获取当前时间
    fmt.Printf("current time:%v\n", now)
    
    year := now.Year()     //年
    month := now.Month()   //月
    day := now.Day()       //日
    hour := now.Hour()     //小时
    minute := now.Minute() //分钟
    second := now.Second() //秒
    fmt.Printf("%d-%02d %02d:%02d:%02d\n", year, month, day, minute, second)
}

2、时间戳

时间戳是自1970年1月1日(08:00:00GMT)至当前时间的总毫秒数。它也被称为Unix时间戳(UnixTimestamp)

基于对象获取时间戳示例:

func timestampDemo() {
    now := time.Now() 
    timestamp1 := now.Unix()
    timestamp2 := now.UnixNano()
    fmt.Printf("current timestamp1:%v\n", timestamp1)
    fmt.Printf("current timestamp2:%v\n", timestamp2)
}

使用time.Unix()函数可以将时间戳转为时间格式。

func timestampDemo2() {
    timeObj := time.Unix(timestamp, 0) //将时间戳转为时间格式
    fmt.Println(timeObj)
    year := timeObj.Year()     //年
    month := timeObj.Month()   //月
    day := timeObj.Day()       //日
    hour := timeObj.Hour()     //小时
    minute := timeObj.Minute() //分钟
    second := timeObj.Second() //秒
    fmt.Printf("%d-%02d-%02d %02d:%02d:%02d\n", year, month, day, hour, minute, second)
}

3、时间间隔

time.Durationtime包定义的一个类型,它代表两个时间点之间经过的时间,以纳秒为单位。time.Duration表示一段时间间隔,可表示的最长时间段大约290年。

time包中定义的时间间隔类型的常量:

const (
	Nanosecond Duration = 1
    Microsecond         = 1000 * Nanosecond
    Millisecond         = 1000 * Microsecond
    Second              = 1000 * Millisecond
    Minute              = 60 * Second
    Hour                = 60 * Minute
)

例如:time.Duration表示1纳秒,time.Second表示1秒

4、时间操作

Add

时间 + 时间间隔的操作,Go语言的时间对象有提供Add方法如下:

func (t Time) Add(d Duration) Time

一个小时后的时间,示例:

func miai() {
    now := time.Now()
    later := now.Add(time.Hour) // 当前时间加1小时后的时间
    fmt.Println(later)
}

Sub

求两个时间之间的差值,示例:

func (t Time) Sub(u Time) Duration

返回一个时间段t-u。如果结果超出了Duration可以表示最大值/最小值,将返回最大值/最小值。要获取时间点t-d(d为Duration),可以使用t.Add(-d)

Equal

func (t Time) Equal(u Time) bool

判断两个时间是否相同,会考虑时区的影响,因此不同时区标准的时间也可以正确比较。本方法和t==u不同,这种方法还会比较地点和时区信息。

Before

func (t Time) Before(u Time) bool

如果t代表的时间点在u之前,返回真;否则返回假。

After

func (t Time) After(u Time) bool

如果t代表的时间点在u之后,返回真;否则返回假。

5、定时器

使用time.Tick(时间间隔)来设置定时器,定时器的本质上是一个通道(channel)。

func tickDemo() {
	ticker := time.Tick(time.Second) //定义一个1秒间隔的定时器
	for i := range ticker {
	 fmt.Println(i) //每秒都会执行的任务
	}
}

6、时间格式化

时间类型有一个自带的方法Format进行格式化,需要注意的是Go语言中格式化时间模板不是常见的Y-m-d H:M:S而是使用Go的诞生时间2006年1月2号15点04分(记忆时间口诀20061234)。

补充:如果想格式化为12小时方式,需指定PM

func formatDemo() {
    now := time.Now()
    // 格式化的模板为Go的出生时间2006年1月2号15点04分 Mon Jan
    // 24小时制
    fmt.Println(now.Format("2006-01-02 15:04:05.000 Mon Jan"))
    // 12小时制
    fmt.Println(now.Format("2006-01-02 03:04:05.000 PM Jan"))
    fmt.Println(now.Format("2006/01/02 15:04"))
    fmt.Println(now.Format("15:04 2006/01/02"))
    fmt.Println(now.Format("2006/01/02"))
}

7、解析字符串格式的时间

now := time.Now()
fmt.Println(now)
// 加载时区
loc, err := time.LoadLocation("Asia/Shanghai")
if err := nil {
    fmt.Println(err)
    return
}
// 按照指定时区和指定格式解析字符串时间
timeObj, err := time.ParseInLocation("2006/01/02 15:04:05", "2019/08/04 14:15:20", loc)
if err != nil{
    fmt.Println(timeObj)
    fmt.Println(timeObj.Sub(now))
}

十六、Go语言文件操作

计算机中的文件是存储在外部介质(通常是磁盘)上的数据集合,文件分为文本文件和二进制文件。

1、打开文件和关闭文件

os.Open()函数能够打开一个文件,返回一个*File和一个err。对得到的文件实例调用close()方法能够关闭文件。

package main
import (
	"fmt"
    "os"
)

func main() {
    // 只读方式打开当前目录下的main.go文件
    file, err := os.Open("./main.go")
    if err != nil {
        fmt.Println("open file failed!, err:", err)
        return
    }
    // 关闭文件
    file.Close()
}

为了防止文件忘记关闭,通常使用defer注册文件关闭语句。

2、读取文件

file.Read()

2.1基本使用

Read方法定义:

func (f *File) Read(b []byte) (n int, err error)

它接收一个字节切片,返回读取的字节数和可能的具体错误,读到文件末尾时会返回0io.EoF。示例:

func main() {
    // 只读方式打开当前文件下的main.go文件
    file, err := os.Open("./main.go")
    if err != nil {
        fmt.Println("open file failed!, err:", err)
        return
    }
    defer file.Close()
    // 使用Read方法读取数据
    var tmp = make([]byte, 128)
    n, err := file.Read(tmp)
    if err == io.EOF {
        fmt.Println("文件读完了")
        return
    }
    if err != nil {
        fmt.Println("read file failed, err:", err)
        return
    }
    fmt.Printf("读取了%d字节数据\n", n)
    fmt.Println(string(tmp[:n]))
}

2.2、循环读取

使用for循环读取文件中的所有数据。

func main() {
    // 只读方式打开文件当前目录下的main.go文件
    file, err := os.Open("./main.go")
    if err != nil {
        fmt.Println("open file failed!, err:", err)
        return
    }
    defer file.Close()
    // 循环读取文件
    var content []byte
    var tmp = make([]byte, 128)
    for {
        n, err := file.Read(tmp)
        if err == io.EOF {
            fmt.Println("文件读完了")
            break
        }
        if err != nil {
            fmt.Println("read file failed, err:", err)
            return
        }
        content = append(content, tmp[:n]...)
    }
    fmt.Println(string(content))
}

2.3、bufio读取文件

bufio是在file的基础上封装了一层API,支持更多的功能。

package main 

import (
	"bufio"
    "fmt"
    "io"
    "os"
)

// bufio按行读取示例
func main() {
    file, err := os.Open("./xx.txt")
    if err != nil {
        fmt.Println("open file failed, err:", err)
        return
    }
    defer file.Close()
    reader := bufio.NewReader(file)
    for {
        line, err := reader.ReadString('\n') //注意是字符
        if err == io.EOF {
            if len(line) != 0 {
                fmt.Println(line)
            }
            fmt.Println("文件读完了")
            break
        }
        if err != nil {
            fmt.Println("read file failed, err:", err)
            return
        }
        fmt.Print(line)
    }
}

2.4、ioutil读取整个文件

io/ioutil包的ReadFile方法能够读取完整的文件,只需要将文件名作为参数传入。

package main

import (
	"fmt"
    "io/ioutil"
)

// ioutil.ReadFile读取整个文件
func main() {
    content, err := ioutil.ReadFile("./main.go")
    if err != nil {
        fmt.Println("read file failed, err:", err)
        return
    }
    fmt.Println(string(content))
}

3、文件写入操作

os.OpenFile()函数能够以指定模式打开文件,从而实现文件写入相关功能。

func OpenFile(name string, flag int, perm FileMode) (*File, error) {
    ...
}

其中:

name:要打开的文件名flag:打开文件的模式。模式有一下几种:

模式 含义
os.O_WRNOLY 只写
os.O_CREATE 创建文件
os.O_RDONLY 只读
os.O_RDWR 读写
os.O_TRUNC 清空
os.O_APPEND 追加

perm:文件权限,一个八进制数。r(读)04,w(写)02,x(执行)01。

4、Write和WriteString

func main() {
    file, err := os.OpenFile("xx.txt", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
    if err != nil {
        fmt.Println("open file failed, err:", err)
        return
    }
    defer file.Close()
    str := "hello eric"
    file.Write([]byte(str))            //写入字节切片数据
    file.WriteString("hello Jingjing") //直接写入字符串数据
}

5、bufio.NewWriter

func main() {
    file, err := os.OpenFile("XX.txt", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
    if err != nil {
        fmt.Println("open file failed, err:", err)
        return
    }
    defer file.Close()
    writer := bufio.NewWriter(file)
    for i := 0; i < 10; i++ {
        writer.WriterString("hello eric\n") //将数据先写入缓存
    }
    writer.Flush() //将缓存中的内容写入文件
}

6、ioutil.WriteFile

func main() {
    str := "hello eric"
    err := ioutil.WriteFile("./xx.txt", []byte(str), 0666)
    if err != nil {
        fmt.Println("write file failed, err:", err)
        return
    }
}

7、copyFile

借助io.Copy()实现一个拷贝文件函数。

// CopyFile 拷贝文件函数
func CopyFile(dstName, srcName string) (written int64, err error) {
    // 以读方式打开源文件
    src, err := os.Open(srcName)
    if err != nil {
        fmt.Printf("open %s failed, err:%v.\n", srcName, err)
        return
    }
    defer src.Close()
    // 以写|创建的方式打开目标文件
    dst, err := os.OpenFile(dstName, os.O_WRONLY|os.O_CREATE, 0644)
    if err != nil {
        fmt.Printf("open %s failed, err:%v.\n", dstName, err)
        return
    }
    fmt.Println("copy done!")
}

8、实现一个cat命令

使用文件操作相关知识,模拟实现linux平台cat命令的功能。

package main

import (
	"bufio"
    "flag"
    "fmt"
    "io"
    "os"
)

// cat命令实现
func cat(r *buffio.Reader) {
    for {
        buf, err := r.ReadBytes('\n') //注意是字符
        if err == io.EOF {
            // 退出之前将已读到的内容输出
            fmt.Fprintf(os.Stdout, "%s", buf)
            break
        }
        fmt.Fprintf(os.Stdout, "%s", buf)
    }
}

func main() {
    flag.Parse() // 解析命令行参数
    if flag.NArg() == 0 {
        // 如果没有默认参数从标准输入读取内容
        cat(bufio.NewReader(os.Stdin))
    }
    // 依次读取每个指定文件的内容并打印到终端
    for i := 0; i < flag.NArg(); i++ {
        f, err := os.Open(flag.Arg(i))
        if err != nil {
            fmt.Fprintf(os.Stdout, "reading from %s failed, err:%v\n", flag.Arg(i), err)
            continue
        }
        cat(bufio.NewReader(f))
    }
}

十七、Json序列化

1、基本的序列化

Go语言中json.Marshal()(序列化)与json.Unmarshal(反序列化)的基本用法。

package main

import (
    "fmt"
    "encoding/json"
)

type Person struct {
    Name   string
    Age    int64
    Weight float64
}

func main() {
    p1 := Person{
        Name: "七米",
        Age: 18,
        Weight: 71.5,
    }
    // struct -> json string
    b, err := json.Marshal(p1)
    if err != nil {
        fmt.Printf("json.Marshal, err:%v\n", err)
        return
    }
    fmt.Printf("str:%s\n", b)
    // json string -> struct
    var p2 Person
    err = json.Unmarshal(b, &p2)
    if err != nil {
        fmt.Printf("json.Unmarshal dailed, err:%v\n", err)
        return
    }
    fmt.Printf("p2:%#v\n", p2)
}

结果:

str:{"Name":"七米","Age":18,"Weight":71.5}
p2:main.Person{Name:"七米", Age:18, Weight:71.5}

2、结构体tag介绍

Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来。Tag在结构体字段的后方定义,由一对反引号包裹起来,示例:

`key1:"value1" key2:"value2"`

结构体tag由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。同一个结构体字段可以设置多个键值对tag,不同的键值对之间使用空格分隔。

3、使用json tag指定字段名

序列化与反序列化默认情况下使用结构体的字段名,可以通过给结构体字段添加tag来指定json序列化的字段名。

// 使用json tag指定序列化与反序列化时的行动
type Person struct {
    Name   string `json:"name"` //指定json序列化/反序列化时使用小写name
    Age    int64
    Weight float64
}

4、忽略某个字段

如果想在json序列化/反序列化时忽略掉结构体中的某个字段,可以按如下方式在tag中添加-

// 使用json tag指定json序列化与反序列化时的行为
type Person struct {
    Name   string `json:"name"` // 指定json序列化/反序列化时使用小写name
    Age    int64
    Weight float64 `json:"-"` // 指定json序列化时忽略此字段
}

5、忽略空值字段

当struct中的字段没有值时,json.Marshal()序列化的时候不会忽略这些字段,而是默认输出字段的类型零值(例如intfloat类型零值是0,string类型零值是"",对象类型零值是nil)。如果需要在序列化时忽略这些没有值的字段,可以在对应字段添加omitemptytag。

示例:

type User struct {
    Name  string   `json:"name"`
    Email string   `json:"email"`
    Hobby []string `json:"hobby"`
}

func omitemptyDemo() {
    u1 := User{
        Name: "奇米",
    }
    // struct -> json string
    b, err := json.Marshal(u1)
    if err := nil {
        fmt.Printf("json.Marshal failed, err:%v\n", err)
        return
    }
    fmt.Printf("str:%s\n", b)
}

输出结果:

str:{"name":"奇米","email":"","hobby":null}

如果需要在最终的序列化结果中去掉空值字段,可以像下面这样定义结构体:

// 在tag中添加omitempy忽略空值
// 注意这里hobby,omitempty 合起来是json tag值,中间用英文逗号分隔
type User struct {
    Name  string   `json:"name"`
    Email string   `json:"email,omitempty"`
    Hobby []string `json:"hobby,omitempty"`
}

此时,执行上述的omitemptyDemo,输出结果如下:

str:{"name":"奇米"} // 序列化结果中没有email和hobby字段

6、忽略嵌套结构体空值字段

首先来看几种结构体嵌套的示例:

type User struct {
    Name  string   `json:"name"`
    Email string   `json:"email,omitempty"`
    Hobby []string `json:"hobby,omitempty"`
    Profile
}

type Profile struct {
    Website string `json:"site"`
    Slogan  string `json:"slogan"`
}

func nestedStructDemo() {
    u1 := User{
        Name: "奇米",
        Hobby: []string{"足球", "双色球"},
    }
    b, err := json.Marshal(u1)
    if err != nil {
        fmt.Printf("json.Marshal failed, err:%v\n", err)
        return
    }
    fmt.Printf("str:%s\n", b)
}

匿名嵌套Profile时序列化后的json串为单层的:

str:{"name":"奇米","hobby":["羽毛球","台球"],"site":"","slogan":""}

想要变成嵌套的json串,需要改为具名嵌套或定义字段tag:

type User struct {
    Name    string   `json:"name"`
    Email   string   `json:"email.omitempty"`
    Hobby   []string `json:"hobby,omitempty"`
    Profile `json:"profile"`
}
// str:{"name":"奇米",“hobby”:["足球","双色球"],"profile":{"site":"","slogan":""}}

想要在嵌套的结构体为空值时,忽略该字段,仅添加omitempty是不够的:

type User struct{
    Name     string   `json:"name"`
    Email    string   `json:"email,omitempty"`
    Hobby    []string `json:"hobby,omitempty"`
    Profile `json:"profile,omitempty"`
}
// str:{"name":"奇米","hobby":["足球","双色球"],"profile":{"site":"","slogan":""}}

还需要使用嵌套结构体指针:

type User struct {
    Name     string   `json:"name"`
    Email    string   `json:"email,omitempty"`
    Hobby    []string `json:"hobby,omitempty"`
    *Profile `json:"Profile,omitempty"`
}
// str:{"name":"奇米","hobby":["足球","双色球"]}

7、不修改原结构体忽略空值字段

我们需要json序列化User,但是不想把密码也序列化,又不想修改User结构体,这个时候可以使用创建另外一个结构体PublicUser匿名嵌套原User,同时指定Password字段为匿名结构体指针类型,并添加omitempty tag,示例代码:

type User struct {
    Name     string `json:"name"`
    Password string `json:"password"`
}

type PublicUser struct {
    *User    // 匿名字段
    Password *struct{} `json:"password,omitempty"`
}

func omitPasswordDemo() {
    u1 := User{
        Name:     "奇米"
        Password: "123456"
    }
    b, err := json.Marshal(PublicUser{User: &u1})
    if err != nil {
        fmt.Printf("json.Marshal u1 failed, err:%v\n", err)
        return
    }
    fmt.Printf("str:%s\n", b)  // str:{"name":"奇米"}
}

8、优雅处理字符串格式的数字

有时候,前端在传递来的json数据中坑能会使用字符串类型的数字,这个时候可以在结构体tag中添加string来告诉jdon包从字符串中解析相应字段的数据:

type Card struct {
    ID    int64   `json:"id,string"`    // 添加string tag
    Score float64 `json:"score,string"` // 添加string tag
}

func intAndStringDemo() {
    jsonStr1 := `{"id": "123456", "score": "88.50"}`
    var c1 Card
    if err := json.Unmarshal([]byte(jsonStr1), &c1); err != nil {
        fmt.Printf("json.Unmarsha jsonStr1 failed, err:%v\n", err)
        return
    }
    fmt.Printf("c1:%#v\n", c1) // c1:main.Card{ID:123456, Score:88.5}
}

9、整形变浮点数

在JSON协议中是没有整型和浮点型之分的,它们统称为number。json字符串的数字经过Go语言中的json包反序列化之后都会成为float64类型。示例:

func jsonDemo() {
    // map[string]interfance{} -> json string
    var m = make(map[string]interfance{}, 1)
    m["count"] = 1 // int
    b, err != nil {
        fmt.printf("marshal dailed, err:%v\n", err)
    }
    fmt.Printf("str:%#v\n", string(b))
    // json string -> map[string]interface{}
    var m2 map[string]interfance{}
    err = json.Unmarshal(b, $m2)
    if err != nil {
        fmt.Printf("unmarshal failed, err:%v\n", err)
        return
    }
    fmt.Printf("value:%v\n", m2["count"]) // 1
    fmt.Printf("type:%T\n", m2["count"])  // float64
}

这种场景下如果想要更合理的处理数字就需要使用decode去反序列化,示例:

func decoderDemo() {
    // map[string]interfance{} -> json string
    var m = make(map[string]interface{}, 1)
    m["count"] = 1 // int
    b, err := json.Marshal(m)
    if err != nil {
        fmt.Printf("marshal failed, err:%V\n", err)
    }
    fmt.Printf("str:%#v\n", string(b))
    // json string -> map[string]interface{}
    var m2 map[string]interface{}
    // 使用decoder方式反序列化,指定使用number类型
    decoder := json.NewDecoder(bytes.NewReader(b))
    decoder := UseNumber()
    err = decoder.Decode(&m2)
    if err != nil {
        fmt.Printf("unmarshal failed, err:%v\n", err)
        return
    }
    fmt.Printf("value:%v\n", m2["count"]) // 1
    fmt.printf("type:%T\n", m2["count"])  // json.Number
    // 将m2["count"]转为json.Number之后调用Int64()方法获得int64类型的值
    count, err := m2["count"].(json.Number).Int64()
    if err != nil {
        fmt.Printf("parse to int64 failed, err:%v\n", err)
        return
    }
    fmt.Printf("type:%T\n", int(count)) // int
}

json.Number的源码定义如下:

// A Number represents a JSON number literal.
type Number string

// String returns the literal text of the number.
func (n Number) String() string { return string(n) }

// Float64 returns the number as a float64.
func (n Number) Float64() (float64, error) {
	return strconv.ParseFloat(string(n), 64)
}

// Int64 returns the number as an int64.
func (n Number) Int64() (int64, error) {
	return strconv.ParseInt(string(n), 10, 64)
}

我们在处理number类型的json字段时需要先得到json.Number类型,然后根据该字段的实际类型调用Float64()Int64()

10、自定义解析时间字段

Go语言内置的json包使用RFC3339标准中定义的时间格式,对我们序列化时间字段的时候有很多限制。

type Post struct {
    CreateTime time.Time `json:"create_time"`
}

func timeFieldDemo() {
    p1 := Post{CreateTime: time.Now()}
    b, err := json.Marshal(p1)
    if err != nil {
        fmt.Printf("json.Marshal p1 failed, err:%v\n", err)
        return
    }
    fmt.Printf("str:%s\n", b)
    jsonStr := `{"create_time":"2020-04-05 12:25:42"}`
    var p2 Post
    if err := json.Unmarshal([]byte(jsonStr), &p2); err != nil {
        fmt.Printf("json.Unmarshal failed, err:%v\n", err)
        return
    }
    fmt.Printf("p2:%#v\n", p2)
}

输出结果:

str:{"create_time":"2020-04-05T12:28:06.799214+08:00"}
json.Unmarshal failed, err:parsing time ""2020-04-05 12:25:42"" as ""2006-01-02T15:04:05Z07:00"": cannot parse " 12:25:42"" as "T"

也就是内置的json包不识别我们常用的字符串时间格式,如2020-04-05 12:25:42

不过我们通过实现json.Marshaler/json.Unmarshaler接口实现自定义的时间格式解析。

type CustomTime struct {
    time.Time
}

const ctLayout = "2006-01-02 15:04:05"

var nilTime = (time.Time{}).UnixNano()

func (ct *CustomTime) UnmarshalJSON(b []byte) (err error) {
    s := strings.Trim(string(b), "\"")
    if s == "null" {
        ct.Time = time.Time{}
        return
    }
    ct.Time, err = time.Parse(ctLayout, s)
    return
}

func (ct *CustomTime) MarshalJSON() ([]byte, error){
    if ct.Time.UnixNano() != nilTime {
        return []byte("null"), nil
    }
    return []byte(fmt.Sprintf("\"%s\"", ct.Time.Format(ctLayout))), nil
}

type Post struct {
    CreateTime CustomTime `json:"create_time"`
}

func timeFieldDemo() {
    p1 := Post{CreateTime: CustomeTime{time.Now()}}
    b, err := json.Marshal(p1)
    if err != nil {
        fmt.Printf("json.Marshal p1 failed, err:%v\n", err)
        return
    }
    fmt.Printf("str:%s\n", b)
    jsonStr := `{"create_time": "2020-04-05 12:25:42"}`
    var p2 Post
    if err := json.Unmarshal([]byte(jsonStr), &p2); err != nil {
        fmt.Printf("json.Unmarshal failed, err:%v\n", err)
        return
    }
    fmt.Printf("p2:%#v\n",p2)
}

11、自定义MarshalJSON和UnmarshalJSON方法

上面那种自定义类型的方法稍微啰嗦了一点,下面有一种相对便捷的方法。

如果能够为某个类型实现了MarshalJSON()([]byte, error)UnmarshalJSON(b []byte) error方法,那么这个类型在序列化(MarshalJSON)/反序列化(UnmarshalJSON)时就会使用你定制的相应方法。

type Order struct {
    ID          int       `json:"id"`
    Title       string    `json:"title"`
    CreatedTime time.Time `json:"created_time"`
}

const layout = "2006-01-02 15:04:05"

// MarshalJSON 为Order类型实现自定义的MarshalJSON方法
func (o *Order) MarshalJSON() ([]byte, error) {
    type TempOrder Order // 定义与Order字段一致的新类型
    return json.Marshal(struct {
        CreatedTime string `json:"created_time"`
        *TemoOrder         // 避免直接嵌套Order进入死循环
    }{
        CretedTime: o.CreatedTime.Format(layout),
        TempOrder:  (*TempOrder)(o),
    })
}

// UnmarshalJSON 为Order类型实现自定义的UnmarshallJSON方法
func (o *Order) UnmarshalJSON(data []byte) error {
    type TempOrder Order // 定义与Order字段一致的新类型
    ot := struct {
        CreatedTime string `json:"created_time"`
        *TempOrder       // 避免直接嵌套Order进入死循环 
    }{
        TempOrder: (*TempOrder)(o),
    }
    if err := json.Unmarshal(data, &ot); err != nil {
        return err
    }
    var err error
    o.CreatedTime, err = time.Parse(layout, ot.CreatedTime)
    if err != nil {
        return err
    }
    retirn nil
}

// 自定义序列化方法
func customMethodDemo() {
    o1 := Order{
        ID:          123456,
        TiTle:       "《奇米的Go学习笔记》",
        CreatedTime: time.Now(),
    }
    // 通过自定义的MarshalJSON方法实现struct -> json string
    b, err := json.Marshal(&o1)
    if err != nil {
        fmt.Printf("json.Marshal o1 failed, err:%v\n", err)
        return
    }
    fmt.Printf("str:%s\n", b)
    // 通过自定义的UnmarshalJSON方法实现JSON string -> struct
    jsonStr := `{"created_time":"2020-04-05 10:18:20", "id":"123456","title":"《奇米的Go学习笔记》"}`
    var o2 Order
    if err := json.Unmarshal([]byte(jsonStr), &o2); err != nil {
        fmt.Printf("json.Unmarshal failed, err:%v\n", err)
        return
    }
    fmt.Printf("o2:%#v\n", o2)
}

输出结果:

str:{"created_time":"2020-02-05 10:32:20","id":123456,"title":"《奇米的Go学习笔记》"}
o2:main.Order{ID:123456, Title:"《奇米的Go学习笔记》", CreatedTime:time.Time{wall:0x0, ext:63721678700, loc:(*time.Location)(nil)}}

12、使用匿名结构体添加字段

使用内嵌结构体能够扩展结构体的字段,但有时候我们没有必要单独定义新的结构体,可以使用匿名结构体简化操作:

type UserInfo struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

func anonymousStructDemon() {
    u1 := UserInfo{
        ID:   123456,
        Name: "奇米",
    }
    // 使用匿名结构体内嵌User并添加额外字段Token
    b, err := json.Marshal(struct {
        *UserInfo
        Token string `json:"token"`
    }{
        &u1,
        "91je3a4s72d1da96h",
    })
    if err != nil {
        fmt.Printf("json.Marsha failed, err:%v\n", err)
        return
    }
    fmt.Printf("str:%s\n", b)
    // str:{"id":123456,"name":"奇米","token":"91je3a4s72d1da96h"}
}

13、使用匿名结构体组合多个结构体

同理,也可以使用匿名结构体来组合多个结构体来序列化与反序列化数据:

type Comment struct {
    Content string
}

type Image struct {
    Title string `json:title`
    URL   string `json:"url"`
}

func anonymousStructDemo2() {
    c1 := Comment{
        Content: "永远不要高估自己",
    }
    i1 := Image{
        Title: "点赞",
        URL:   "https://www.cnblogs.com/wylshkjj/p/14204402.html"
    }
    // struct -> json string
    b, err := json.Marshal(struct {
        *Comment
        *Image
    }{&c1, &i1})
    if err != nil {
        fmt.Printf("json.Marshal failed, err:%v\n", err)
        return
    }
    fmt.Printf("str:%s\n", b)
    // json string -> struct
    jsonStr := `{"Content":"永远不要高估自己","title":"点赞","url":"https://www.cnblogs.com/wylshkjj/p/14204402.html"}`
    var (
        c2 Comment
        i2 Image
    )
    if err := json.Unmarshal([]byte(jsonStr), &struct{
        *Comment
        *Image
    }{&c2, &i2}); err != nil {
        fmt.Printf("json.Unmarshal failed, err:%v\n", err)
        return
    }
    fmt.Printf("c2:%#v i2:%#v\n", c2, i2)
}

输出:

str:{"Content":"永远不要高估自己","title":"赞赏码","url":"https://www.liwenzhou.com/images/zanshang_qr.jpg"}
c2:main.Comment{Content:"永远不要高估自己"} i2:main.Image{Title:"点赞", URL:"https://www.cnblogs.com/wylshkjj/p/14204402.html"}

14、处理不确定层级的json

如果json串没有固定的格式导致不好定义与其相对应的结构体时,我们可以使用json。RawMessage原始字节数据保存下来。

type sendMsg struct {
    User string `json:"user"`
    Msg  string `json:"msg"`
}

func rawMessageDemo() {
    jsonStr := `{"sendMsg":{"user":"q1mi","msg":"永远不要高估自己"},"say":"Hello"}`
    // 定义一个map, value类型为json.RawMessage, 方便后续灵活地处理
    var data map[string]json.RawMessage
    if err := json.Unmarshal([]byte(jsonStr), &data); err != nil {
        fmt.Printf("json.Unmarshal failed, err:%v\n", err)
        return
    }
    fmt.Printf("msg:%#v\n", msg)
    // msg:main.sendMsg{User:"q1mi", Msg:"永远不要高估自己"}
}

十八、反射

1、变量的内在机制

Go语言中的变量是分为两部分的:

  • 类型信息:预先定义好的元信息。
  • 值信息:程序运行过程中可动态变化的。

2、反射介绍

反射是指在程序运行期对程序本身进行访问和修改的能力。程序在编译时,变量被转换为内存地址,变量名不会被编译器写入到可执行部分。在运行程序时,程序无法获取自身的信息。

支持反射的语言可以在程序编译期将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期获取类型的反射信息,并且有能力修改他们。

Go程序在运行期使用reflect包访问程序的反射信息。

在接口中介绍了空接口,空接口可以存储任意类型的变量,那如何知道空接口保存的数据是什么?反射就是在运行时动态的获取一个变量的类型信息和值信息。

3、reflect包

在Go语言的反射机制中,任何接口值都是由一个具体类型具体类型的值两部分组成的(在博客接口的部分中有介绍相关概念)。在Go语言中反射的相关功能由内置的reflect包提供,任意接口值在反射中都可以理解为由reflect.Typereflect.Value两部分组成,并且reflect包提供了reflect.TypeOfreflect.ValueOf两个函数来获取任意对象的Value和Type。

3.1、TypeOf

在Go语言中,使用reflect.TypeOf()函数可以获得任意值的类型对象(reflect.Type),程序通过类型对象可以访问任意值的类型信息。

package main

import (
    "fmt"
    "reflect"
)

func reflectType(x interfane{}) {
    v := reflect.TypeOf(x)
    fmt.Printf("type:%v\n", v)
}
func main() {
    var a float32 = 3.14
    reflectType(a) // type:float32
    var b int64 = 100
    reflectType(b) // type:int64
}

3.2、type name 和type kind

在反射中关于类型还划分为两种:类型(Type)种类(Kind)。因为在Go语言中我们可以使用type关键字构造很多自定义类型,而种类(kind)就是指底层的类型,但在反射中,当需要区分指针、结构体等大品种的类型时,就会用到种类(kind)。举个例子,定义了两个指针类型和两个结构体类型,通过反射查看它们的类型和种类。

package main

import (
    "fmt"
    "reflect"
)

type myInt int64

func reflectType(x interfance{}) {
    t := reflect.TypeOf(x)
    fmt.Printf("type:%v kind:%v\n", t.Name(), t.Kind())
}

func main() {
    var a *float32 // 指针
    var b myInt    // 自定义类型
    var c rune     // 类型别名
    reflectType(a) // type: kind:ptr
    reflectType(b) // type:myInt kind:int64
    reflectType(c) // type:int32 kind:int32
    
    type person struct {
        name string
        age int
    }
    type book struct{ title string }
    var d = person{
        name: "山河图",
        age:  18,
    }
    var e = book{title:"《跟奇米学Go语言》"}
    reflectType(d) // type:person kind:struct
    reflectType(e) // type:book kind:struct
}

Go语言的反射中像数组、切片、Map、指针等类型的变量,它们的.Name()都是返回

reflect包中定义的Kind类型:

type Kind uint
const (
    Invalid Kind = iota  // 非法类型
    Bool                 // 布尔型
    Int                  // 有符号整型
    Int8                 // 有符号8位整型
    Int16                // 有符号16位整型
    Int32                // 有符号32位整型
    Int64                // 有符号64位整型
    Uint                 // 无符号整型
    Uint8                // 无符号8位整型
    Uint16               // 无符号16位整型
    Uint32               // 无符号32位整型
    Uint64               // 无符号64位整型
    Uintptr              // 指针
    Float32              // 单精度浮点数
    Float64              // 双精度浮点数
    Complex64            // 64位复数类型
    Complex128           // 128位复数类型
    Array                // 数组
    Chan                 // 通道
    Func                 // 函数
    Interface            // 接口
    Map                  // 映射
    Ptr                  // 指针
    Slice                // 切片
    String               // 字符串
    Struct               // 结构体
    UnsafePointer        // 底层指针
)

3.3、ValueOf

reflect.ValueOf()返回的是reflect.Value类型,其中包含了原始值的值信息。reflect.Value与原始值之间可以互相转换。

reflect.Value类型提供的获取原始值的方法如下:

方法 说明
Interface() interface{} 将值以interface{}类型返回,可以通过类型断言转换为指定类型
Int() uint64 将值以int类型返回,所有有符号整型均可以此方式返回
Uint() uint64 将值以uint类型返回,所有无符号整形均可以此方式返回
Float() float64 将值以双精度(float64)类型返回,所有浮点数(float32、float64)均可以此方式返回
Bool() bool 将值以bool类型返回
Bytes() []bytes 将值以字节数组[]bytes类型返回
String() string 降值以字符串类型返回

3.4、通过反射获取值

func reflectValue(x interface()) {
    v := reflect.ValueOf(x)
    k := v.Kind()
    switch k {
    case reflect.Int64:
        // v.Int()从反射中获取整形的原始值,然后通过int64()强制类型转换
        fmt.Printf("type is int64, value is %d\n", int64(v.Int()))
    case reflect.Float64:
        // v.Float()从反射中获取浮点型的原始值,然后通过float64()强制类型转换
        fmt.Printf("type is flost64, value is %f\n", float64(v.Float()))
    }
}
func main() {
    var a float32 = 3.14
    var b int64 = 100
    reflectValue(a) // type is float32, value is 3.140000
    reflectValue(b) // type is int64, value is 100
    // 将int类型的原始值转换为reflect.Value类型
    c := reflect.Value类型
    c := reflect.ValueOf(10)
    fmt.Printf("type c :%T\n", c) // type c :reflect.Value
}

3.5、通过反射设置变量的值

想要在函数中通过反射修改变量的值,需要注意函数参数传递的是值拷贝,必须传递变量地址才能修改变量值。而反射中专有的Elem方法来获取指针对应的值。

package main
import (
    "fmt"
    "reflect"
)
func reflectSetValue1(x interfance{}) {
    v := reflect.ValueOf(x)
    if v.Kind() == reflect.Int64 {
        v.SetInt(200) //修改的是副本,reflect包会引发panic6
    }
}
func reflectSetValue2(x interface{}) {
    v := reflect.ValueOf(x)
    // 反射中使用Elem()方法获取指针对应的值
    if v.Elem().Kind() == reflect.Int64 {
        v.Elem().SetInt(200)
    }
}
func main() {
    var a int64 = 100
    // reflectSetValue1(a) // panic: reflect: reflect.Value.SetInt using unaddressable value
    reflectSetValue2(&a)
    fmt.Println(a)
}

3.6、isNil()和isValid()

isNil()

func (v Value) IsNil() bool

isNil()报告v持有的值是否为nil。v持有的值的分类必须是通道、函数、接口、映射、指针、切片之一;否则IsNil函数会导致panic。

isValid()

func (v Value) IsValid() bool

IsValid()返回v是否持有一个值。如果v是Value零值会返回假,此时v除了IsValid、String、Kind之外的方法都会导致panic。

示例:

IsNil()常被用于判断指针是否为空;IsValid()常被用于判定返回值是否有效。

func main() {
    // *int类型空指针
    var a *int
    fmt.Println("var a *int IsNil", reflect.ValueOf(a).IsNil())
    // nil值
    fmt.Println("nil IsValid:", reflect.ValueOf(nil).IsValid())
    // 实例化一个匿名结构体
    b := struct{}{}
    // 尝试从结构体中查找"abc"字段
    fmt.Println("不存在的结构体成员:", reflect.ValueOf(b).FieldByName("abc").IsValid())
    // 尝试从结构体中查找"abc"方法
    fmt.Ptrintln("不存在的结构体方法:", reflect.ValueOf(b).MethodByName("abc").IsValid())
    // map
    c := map[string]int{}
    // 尝试从map中查找一个不存在的键
    fmt.Println("map中查找一个不存在的键")
    fmt.Println("map中不存在的键:", reflect.ValueOf(c).MapIndex(reflect.ValueOf("娜扎")).IsValid())
}

4、结构体反射

4.1、与结构体相关的方法

任意通过reflect.TypeOf()获得反射对象信息后,如果他的类型是结构体,可以通过反射值对象(reflect.Type)的NumField()Field()方法获得结构体成员的详细信息。

reflect.Type中与获取结构提成员相关的方法如下:

方法 说明
Field(i int)StructField 根据索引,返回索引对应的结构体字段的信息。
NumField() int 返回结构体成员字段数量。
FieldByName(name string) (StructField, bool) 根据给定字符串返回对应的结构体字段的信息。
FieldByIndex(index []int) StructField 多层成员访问时,根据[]int提供的每个结构体的字段索引,返回字段的信息。
FieldByNameFunc(match func(string) bool) (StructField, bool) 根据传入的匹配函数匹配需要的字段。
NumMethod() int 返回该类型的方法集中方法的数目
Method(int) Method 返回该类型方法集中的第i个方法
MethodByName(string)(Method,bool) 根据方法名返回该类型方法集中的方法

4.2、StructField类型

StructField类型用来描述结构体中的一个字段的信息。

StructField的定义如下:

type StructField struct {
    // Name是字段的名字。PkgPath是非导出字段的包路径,对导出字段该字段为""。
    // 参见http://golang.org/ref/spec#Uniqueness_of_identifiers
    Name    string
    PkgParh string
    Type      Type      // 字段的类型
    Tag       StructTag // 字段的标签
    Offset    uintptr   // 字段在结构体中的字节偏移量
    Index     []int     // 用于Type.FieldByIndex时的索引切片
    Anonymous bool      // 是否匿名字段
}

4.3、结构体反射示例

当我们使用反射得到一个结构体数据之后可以通过索引依次获取期字段信息,也可以通过字段名去获取指定的字段信息。

type student struct {
    Name  string `json:"name"`
    Score int    `json:"score"`
}

func main() {
    stu1 := student{
        Name: "eric",
        Score: 90,
    }
    t := reflect.TypeOf(stu1)
    fmt.Println(t.Name(), t.Kind()) // student struct
    // 通过for循环遍历结构体的所有字段信息
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        fmt.Printf("name:%s index:%d type:%v json tag:%v\n", field.Name, field.Index, field.Type, field.Tag.Get("json"))
    }
    
    // 通过字段名获取指定结构体字段信息
    if scoreField, ok := t.FieByName("Score"); ok {
        fmt.Printf("name:%s index:%d type:%v json tag:%v\n", scoreField.Name, scoreField.Index, scoreField.Type, scoreField.Tag.Get("json"))
    }
}

接下来编写一个函数printMethod(s interface{})来遍历打印s包含的方法。

// 给student添加两个方法 Study和Sleep(注意瘦子米大写)
func (s student) Study() string {
    msg := "好好学习,天天向上。"
    fmt.Println(msg)
    return msg
}

func (s student) Sleep() string {
    msg := "好好睡觉,快快长大。"
    fmt.Println(msg)
    return msg
}

func printMethod(x interfance{}) {
    t := reflect.TypeOf(x)
    v := reflect.ValueOf(x)
    
    fmt.Println(t.NumMethod())
    for i := 0; i < v.NumMethod(); i++ {
        methodType := v.Method(i).Type()
        fmt.Printf("method name:%s\n", t.Method(i).Name)
        fmt.Printf("method:%s\n", methodType)
        // 通过反射调用方法传递的参数必须是 []reflect.Value 类型
        var args = []reflect.Value{}
        v.Method(i).Call(args)        
    }
}

5、反射是把双刃剑

反射是一个强大并富有表现力的工具,能让我们写出灵活的代码。但是反射不应该被滥用,原因有三个:

  1. 基于反射的代码是极其脆弱的,反射中的类型错误会在真正运行的时候才会引发panic,那很可能是在代码写完的很长时间之后。
  2. 大量使用反射的代码通常难以理解。
  3. 反射的性能低下,基于反射实现的代码通常比正常代码运行速度慢一到两个数量级。

十九、并发

1、并发与并行

并发:同一时段内执行多个任务

并行:同一时刻执行多个任务

Go语言的并发通过goroutine实现。goroutine类似于线程,属于用户态的线程,我们可以根据需要创建成千上万个goroutine并发工作。goroutine是由Go语言的运行时(runtime)调度完成,而线程是由操作系统调度完成。

Go语言还提供channel在多个goroutine间进行通信。goroutinechannel是Go语言秉承的CSP(Communicating Sequential Process)并发模式的重要实现基础。

2、goroutine

在java/c++中我们要实现并发编程的时候,我们通常需要自己维护一个线程池,并且需要自己去包装一个又一个的任务,同时需要自己去调度线程执行任务并维护上下文切换,这一切通常会耗费大量的精力。所以出现了这样一种机制,我们只需要定义很多个任务,让系统去帮助我们把这些任务分配到CPU上实现并发执行。

Go语言中的goroutine就是这样一种机制,goroutine的概念类似于线程,但goroutine是由Go的运行时(runtime)调度和管理的。Go程序会智能的将goroutine中的任务合理的分配给每个CPU。Go语言之所以被称为现代化的编程语言,就是因为它在语言层面已经内置了调度和上下文切换的机制。

在Go语言编程中你不需要去自己写进程、线程、协程,你所使用的只有一个goroutine,当你需要让某个任务并发执行的时候,你只需要把这个任务包装成一个函数,开启一个goroutine去执行这个函数就可以了,就是这么简单粗暴。

2.1、使用goroutine

Go语言中使用goroutine非常简单,只需要在调用函数的时候在前面加上go关键字,就可以为一个函数创建一个goroutine

一个goroutine必定对一个函数,可以创建多个goroutine去执行相同的函数。

2.2、启动单个goroutine

启动goroutine的方式非常简单,只需要在调用的函数(普通函数和匿名函数)前面加上一个go关键字。

示例:

func hello() {
    fmt.Println("Hello Goroutine!")
}
func main() {
    hello()
    fmt.Println("main goroutine done!")
}

示例中hello函数和下面的语句是串行的,执行的结果是打印完Hello Goroutine!后打印main goroutine done!

接下来在调用hello函数前面加上关键字go,也就是启动一个goroutine去执行hello这个函数。

func main() {
    go hello() // 启动另外一个goroutine去执行hello函数
    fmt.Println("main goroutine done!")
}

执行结果只打印了main goroutine done!,并没有打印Hello Goroutine!

在程序启动时,Go程序就会为main()函数创建一个默认的goroutine

当main()函数返回的时候该goroutine就结束了,所有在main()函数中启动的goroutine会一同结束。也就是说main函数所在的goroutine执行结束,其他的goroutine都会结束。

所以如果需要让main函数等hello函数,最简单粗暴的方式就是time.sleep阻塞。

func main() {
    go hello() // 启动另外一个goroutine去执行hello函数
    fmt.Println("main goroutine done!")
    time.Sleep(time.Second)
}

执行上面的代码第一次打印的是main goroutine done!,然后打印Hello Goroutine!

time.sleep让程序阻塞等待在那里一秒,hello在这一秒钟执行完毕,然后主函数main返回,程序未结束。

2.3、启动多个goroutine

在Go语言中实现并发就是这样简单,我们还可以启动多个goroutine

示例:

package main

import (
	"fmt"
    "sync"
)

var wg sync.WaitGroup

func hello(i int) {
    defer wg.Done() // goroutine结束就登记-1
    fmt.Println("Hello Goroutine!", i)
}

func main() {
    for i:=0;i<10;i++ {
        wg.Add(1) // 启动一个goroutine就登记+1
        go hello(i)
    }
    wg.Wait() // 等待所有登记的goroutine都执行完
}

多次执行上面的代码,结果打印的数字都不一致。因为10个goroutine是并发执行的,而goroutine的调度是随机的。

3、goroutine与线程

3.1、可增长的栈

OS线程(操作系统线程)一般都有固定的栈内存(通常为2MB),一个goroutine的栈在其生命周期开始时只有很小的栈(典型情况下2KB),goroutine的栈不是固定的,它可以按需增大和缩小,goroutine的栈大小限制可以达到1GB,虽然极少会用到这么大。所以在Go语言中一次创建10万左右的goroutine也是可以的。

3.2、goroutine调度

GMP是Go语言运行时(runtime)层面的实现,是go语言自己实现的一套调度系统。区别于操作系统调度OS线程。

  • G是个goroutine,里面除了存放本goroutine信息外还有与所在P的绑定信息。
  • P管理着一组goroutine队列,P里面会存储当前goroutine运行的上下文环境(函数指针,堆栈地址及地址边界),P会对自己的管理的goroutine队列做一些调度(比如把占用CPU时间较长的goroutine暂停、运行后续的goroutine等等)当自己的队列消费完,就去全局队列里面取,如果全局队列里面也消费完了回去其它P队列里前任务。
  • M(machine)是Go运行时(runtime)对操作系统内核线程的虚拟,M与内核线程一般是一一映射的关系,一个goroutine最终是要放到M上执行的;

P与M一般也是一一对应的。它们的关系是:P管理着一组G挂载在M上运行。当一个G长久阻塞在一个M上时,runtime会新建一个M,阻塞G所在的P会把其它的G挂载在新建的M上。当旧的G阻塞完成或者认为其已经死掉时回收旧的M。

P的个数是通过runtime.GOMAXPROCS设定(最大256),Go1.5版本之后默认为物理线程数。在并发量大的时候会增加一些P和M,但不会太多,切换频繁也会造成资源消耗。

单从线程角度分析,Go语言相比其他语言的优势在于OS线程是由OS内核来调度的,goroutine则是由Go运行时(runtime)自己的调度器调度的,这个调度器使用一个称为m:n调度的技术(复用/调度m个goroutine到n个OS线程)。其一大特点是goroutine的调度是在用户态下完成的,不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池,不直接调用系统的malloc函数(除非内存池需要改变),成本比调度OS线程低很多。另一方面充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上,再加上本身goroutine的超轻量,以上种种保证了go调度方面的性能。

3.3、GOMAXPROCS

Go运行时的调度器使用GOMAXPROCS参数来确定使用多少个OS线程来同时执行Go代码。默认值是机器上的CPU核心数。例如在一个8核心的机器上,调度器会把Go代码同时调度到8个OS线程上(GOMAXPROCS是m:n调度中的n)。

Go语言中可以通过runtime.GOMAXPROCS()函数设置当前程序并发时占用的CPU逻辑核心数。

Go1.5版本之前,默认使用的是单核心执行。Go1.5版本之后,默认使用全部的CPU逻辑核心数。

通过将任务分配到不同的CPU逻辑核心上实现并行的效果,示例:

func a() {
    for i:=1;i<10;i++ {
        fmt.Println("A:", i)
    }
}

func b() {
    for i:=1;i<10;i++ {
        fmt.Println("B:", i)
    }
}

func main() {
    runtime.GOMAXPROCS(1)
    go a()
    go b()
    time.Sleep(time.Second)
}

两个任务只有一个逻辑核心,此时是做完一个任务再做另一个任务。将逻辑核心数设置为2,此时两个任务并行执行,示例:

func a() {
    for i:1;i<10;i++ {
        fmt.Println("A:", i)
    }
}

func b() {
    for i:=1;i<10;i++ {
        fmt.Println("B:", i)
    }
}

func main() {
    runtime.GOMAXPROCS(2)
    go a()
    go b()
    time.Sleep(time.Second)
}

Go语言中的操作系统线程和goroutine的关系:

  1. 一个操作系统线程对应用户态多个goroutine。
  2. go程序可以同时使用多个操作系统线程。
  3. goroutine和OS线程是多对多的关系,即m:n。

4、channel

单纯的将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。

虽然可以使用共享内存进行数据交换,但是共享内存再不同的goroutine中容易发生竞态问题。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。

Go语言的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信

如果说goroutine是Go程序并发的执行体,channel就是他们之间的连接。

channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。

Go语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。

4.1、channel类型

channel是一种类型,一种引用类型。声明通道类型的格式:

var 变量 chan 元素类型

示例:

var ch1 chan int   // 声明一个传递整形的通道
var ch2 chan bool  // 声明一个传递布尔型的通道
var ch3 chan []int // 声明一个传递int切片的通道

4.2、创建channel

通道是引用类型,通道类型的空值是nil

var ch chan int
fmt.Println(ch) // <nil>

声明的通道后需要使用make函数初始化之后才能使用。

创建channel的格式:

make(chan 元素类型, [缓冲大小])

channel的缓冲大小是可选的。

示例:

ch4 := make(chan int)
ch5 := make(chan bool)
ch6 := make(chan []int)

4.3、channel操作

通道有发送(send)、接收(receive)和关闭(close)三种操作。

发送和接收都使用<-符号。

定义一个通道:

ch := make(chan int)

发送

将一个值发送到通道中。

ch <- 10 // 把10发送到ch中

接收

从一个通道中接收值。

x := <- ch // 从ch中接收值并赋值给变量x
<- ch      // 从ch中接受值,忽略结果

关闭

通过调用内置的close函数来关闭通道。

close(ch)

关于关闭通道需要注意:只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。

关闭后的通道有以下特点:

  1. 对一个关闭的通道再发送值就会导致panic。
  2. 对一个关闭的通道进行接收会一直获取值直到通道为空。
  3. 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
  4. 关闭一个已经关闭的通道会导致panic。

4.4、无缓冲的通道

无缓冲的通道又称为阻塞的通道。示例:

func main() {
    ch := make(chan int)
    ch <- 10
    fmt.Println("发送成功")
}

上面代码能够通过编译,但是执行出现以下报错:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
        .../src/github.com/Q1mi/studygo/day06/channel02/main.go:8 +0x54

出现了死锁deadlock问题

使用ch := make(chan int)创建的是无缓冲的通道,无缓冲的通道只有在有人接收值的时候才能发送值。简单来说就是无缓冲的通道必须有接收才能发送。

上面代码会阻塞在ch <- 10这一行代码形成死锁,解决方式:启用一个goroutine去接收值,示例:

func recv(c chan int) {
    ret := <- c
    fmt.Println("接收成功", ret)
}

func main() {
    ch := make(chan int)
    go recv(ch) // 启用goroutine从通道接收值
    ch <- 10
    fmt.Println("发送成功")
}

无缓冲通道上的发送操作会阻塞,直到另一个goroutine在该通道上执行接收操作,这时值才能发送成功,两个goroutine将继续执行。相反,如果接收操作先执行,接收方的goroutine将阻塞,直到另一个goroutine在该通道上发送一个值。

使用无缓冲通道进行通信将导致发送和接收的goroutine同步化。因此,无缓冲通道也被称为同步通道

4.5、有缓冲的通道

解决上面问题的方法还有一种是使用有缓冲区的通道。在使用make函数初始化通道的时候为其指定通道的容量,示例:

func main() {
    ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道
    ch <- 10
    fmt.Println("发送成功")
}

只要通道的容量大于零,那么该通道就是有缓冲的通道,通道的容量表示通道中能存放元素的数量。

可以使用内置的len函数获取通道内元素的数量,使用cap函数获取通道的容量,不过很少这么做。

4.6、for range从通道循环取值

当向通道中发送完整数据时,我们可以通过close函数来关闭通道。

当通道被关闭时,再往该通道发送值会引发panic,从该通道取值的操作会先取完通道中的值,然后取到的值一直都是对应类型的零值。

示例:

// channel 
func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)
    // 开启goroutine将0~100的数发送到ch1中
    go func() {
        for i:=0;i<100;i++ {
            ch1 <- i
        }
        close(ch1)
    }()
    // 开启goroutine从ch1中接收值,并将该值的平方发送到ch2中
    go func() {
        for {
            i, ok := <-ch1 // 通道关闭后再取值ok=false
            if !ok{
                break
            }
            ch2 <- i*i
        }
        close(ch2)
    }()
    // 在主goroutine中从ch2中接收值打印
    for i := range ch2 { // 通道关闭后会退出for range循环
        fmt.Println(i)
    }
}

示例中有两种方式在接收值的时候判断该通道是否被关闭,不过我们通常使用的是for range的方式。使用for range遍历通道,当通道被关闭的时候就会退出for range

4.7、单向通道

有时我们会将通道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用通道都会对其进行限制,比如限制通道在函数中只能发送或只能接收。

Go语言中提供了单向通道来处理这种情况,示例:

func counter(out chan <- int) {
    for i:=0;i<100;i++ {
        out <- i
    }
    close(out)
}

func squarer(out chan <- int, in <- chan int) {
    for i := range in {
        out <- i * i
    }
    close(out)
}

func printer(in <-chan int) {
    for i := range in {
        fmt.Println(i)
    }
}

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)
    go counter(ch1)
    go squarer(ch2, ch1)
    printer(ch2)
}

其中:

  • chan<- int是一个只写单向通道(只能对其写入int类型值),可以对其执行发送操作但是不能接收操作;
  • <-chan int是一个只读单向通道(只能从其读取int类型值),可以对其执行接收操作但是不能执行发送操作。

在函数传参及任何赋值操作中可以将双向通道转换为单向通道,但反过来是不行的。

4.8、通道总结

channel常见的异常总结,表:

channel nil 非空 空的 满了 没满
接收 阻塞 接收值 阻塞 接收值 接收值
发送 阻塞 发送至 发送值 阻塞 发送值
关闭 panic 关闭成功后,读完数据后返回零值 关闭成功,返回零值 关闭成功,读完数据后返回零值 关闭成功,读完数据后返回零值

注:关闭已经关闭的channel也会引发panic

5、worker poll(goroutine池)

在工作中我们通常会使用可以指定启动的goroutine数量-worker pool模式,控制goroutine的数量,防止gotoutine泄漏和暴涨。

一个简单的work pool示例代码如下:

func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        fmt.Printf("worker:%d end job:%d\n", id, j)
        time.Sleep(time.Second)
        fmt.Printf("worker:%d end job:%d\n", id, j)
        results <- j * 2
    }
}

func main() {
    jobs := make(chan int, 100)
    results := make(chan int, 100)
    // 开启3个goroutine
    for w:=1;w<=3;w++ {
        go worker(w, job, results)
    }
    // 5个任务
    for j:=1;j<=5;j++ {
        jobs <- j
    }
    close(jobs)
    // 输出结果
    for a:=1;a<=5;a++ {
        <-results
    }
}

6、select多路复用

在某些场景下我们需要同时从多个通道接收数据。通道在接收数据时,如果没有数据可以接收将会发生阻塞。这时候可以通过遍历实现效果,示例:

for {
    // 尝试从ch1接收值
    data, ok := <-ch1
    // 尝试从ch2接收值
    data, ok := <-ch2
    ...
}

这种方式虽然可以实现从多个通道接收值的需求,但是运行性能会差很多。为了应对这种场景,Go内置了select关键字,可以同时响应多个通道的操作。

select的使用类似于switch语句,它有一系列case分支和一个默认的分支。每个case会对应一个通道的通信(接收或发送)过程。select会一直等待,直到某个case的通信操作完成时,就会执行case分支对应的语句。示例:

select{
    case <-ch1:
    ...
    case data := <-ch2:
    ...
    case ch3<-data:
    ...
    default:
    默认操作
}

select使用示例:

func main() {
    ch := make(chan int, 1)
    for i:=0;i<10;i++ {
        select{
            case x:=<-ch:
                fmt.Println(x)
            case ch<-i:
        }
    }
}

使用select语句能提高代码的可读性。

  • 可处理一个或多个channel的发送/接收操作。
  • 如果多个case同时满足,select会随机选择一个。
  • 对于没有caseselect{}会一直等待,可用于阻塞main函数。

7、并发安全和锁

有时候在Go代码中可能会存在多个goroutine同时操作一个资源(临界区),这种情况会发生竞态问题(数据竞态)。

示例:

var x int64
var wg sync.WaitGroup

func add() {
    for i:=0;i<5000;i++ {
        x = x + 1
    }
    wg.Done()
}

func main() {
    wg.Add(2)
    go add()
    go add()
    wg.Wait()
    fmt.Println(x)
}

上面代码中我们开启了两个goroutine去累加变量x的值,这两个goroutine在访问和修改x变量的时候就会存在数据竞争,导致最后的结果与期待不符。

7.1、互斥锁

互斥锁是一种常见的控制共享资源访问的方法,它能够保证同时只有一个goroutine可以访问共享资源。Go语言中使用sync包的Mutex类型来实现互斥锁。使用互斥锁来修复上面代码的问题:

var x int64
var wg sync.WaitGroup
var lock sync.Mutex

func add() {
    for i:=0;i<5000;i++ {
        lock.Lock() // 加锁
        x = x + 1
        lock.Unlock() // 解锁
    }
    wg.Done()
}
func main() {
    wg.Add(2)
    go add()
    go add()
    wg.Wait()
    fmt.Println(x)
}

使用互斥锁能够保证同一时间有且只有一个goroutine进入临界区,其它的goroutine则在等待锁;当互斥锁释放后,等待的goroutine才可以获取锁进入临界区,多个goroutine同时等待一个锁时,唤醒的策略是随机的。

7.2、读写互斥锁

互斥锁是完全互斥的,但是有很多实际的场景下是读多写少的,当我们并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的,这种场景下使用读写锁是更好的一种选择。读写锁在Go语言中使用sync包中的RWMutex类型。

读写锁分为两种:读锁和写锁。当一个goroutine获取读锁之后,其他的goroutine如果是获取读锁会继续获得锁,如果是获取写锁就会等待;当一个goroutine获取写锁之后,其它的goroutine无论是获取读锁还是写锁都会等待。

读写锁示例:

var (
	x      int64
    wg     sync.WaitGroup
    lock   sync.Mutex
    rwlock sync.RWMutex
)

func write() {
    // lock.Lock()  // 加互斥锁
    rwlock.Lock() // 加写锁
    x = x + 1
    time.Sleep(10 + time.Millisecond) // 假设读操作耗时10毫秒
    rwlock.Unlock() // 解写锁
    // lock.Unlock() // 解互斥锁
    wg.Done()
}

func read() {
    // lock.Lock() // 加互斥锁
    rwlock.RLock() // 加读锁
    time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒
    rwlock.RUnlock() // 解读锁
    // lock.Unlock() // 解互斥锁
    wg.Done()
}

func main() {
    start := time.Now()
    for i:=0; i<10; i++ {
        wg.Add(1)
        go write()
    }
    for i:=0;i<1000;i++ {
        wg.Add(1)
        go read()
    }
    wg.Wait()
    end := time.Now()
    fmt.Println(end.Sub(start))
}

需要注意的是读写锁非常适合读多写少的场景,如果读和写的操作差别不大,读写锁的优势就发挥不出来。

7.3、sync.WaitGroup

在代码中生硬的使用time.Sleep肯定是不适合的,Go语言中可以使用sync.WaitGroup来实现并发任务的同步。sync.WaitGroup有以下几个方法:

方法名 功能
(wg.WaitGroup)Add(delta int) 计数器+delta
(wg.WaitGroup)Done() 计数器-1
(wg.WaitGroup)Wait() 阻塞直到计数器变为0

sync.WaitGroup内部维护着一个计数器,计数器的值可以增加和减少。例如当我们启动了N个并发任务时,就将计数器值增加N。每个任务完成时通过调用Done()方法将计数器减1。通过调用Wait()来等待并发任务执行完,当计数器值为0时,表示所有并发任务已经完成。

利用sync.WaitGroup将上面的代码优化一下:

var wg sync.WaitGroup

func hello() {
    defer wg.Done()
    fmt.Println("Hello Goroutine!")
}

func main() {
    eg.Add(1)
    go hello() // 启动另外一个goroutine去执行hello函数
    fmt.Println("main goroutine done!")
    wg.Wait()
}

需要注意sync.WaitGroup是一个结构体,传递的时候要传递指针。

7.4、sync.Once

进阶知识点

在编程的很多场景下需要确保某些操作在高并发下的场景只执行一次,例如只加载一次配置文件、只关闭一次通道等。

Go语言中的sync包中提供了一个针对只执行一次场景的解决方案-sync.Once

sync.Once只有一个Do方法,其签名如下:

func (o *Once) Do(f func()) {}

备注:如果要执行函数f需要传递参数就需要搭配闭包来使用。

加载配置文件示例

延迟一个开销很大的初始化操作到真正用到它的时候再执行是一个很好的实践。因为预先初始化一个变量(比如在init函数中完成初始化)会增加程序的启动耗时,而且有可能实际执行过程中这个变量没有用上,那么这个初始化操作就不是必需要做的。示例:

var icons map[string]image.Image

func loadIcons() {
    icons = map[string]image.Image{
        "left":  loadIcon("left.png"),
        "up":    loadIcon("up.png"),
        "right": loadIcon("right.png"),
        "down":  loadIcon("down.png"),
    }
}

// Icon 被多个goroutine调用时不是并发安全的
func Icon(name string) image.Image {
    if icons == nil {
        loadIcons()
    }
    return icons[name]
}

多个goroutine并发调用Icon函数时不是并发安全的,现代的编译器和CPU可能会在保证每个goroutine都满足串行一致的基础上自由地重排访问内存的顺序。loadIcorns函数可能会被重排为一下结果:

func loadIcons() {
    icons = make(map[string]image.Image)
    icons["left"] = loadIcon("left.png")
    icons["up"] = loadIcon("up.png")
    icons["right"] = loadIcon("right.png")
    icons["down"] = loadIcon("down.png")
}

在这种情况下就会出现即使判断了icons不是nil也不意味着变量初始化完成了。考虑到这种情况,我们能想到的办法就是添加互斥锁,保证初始化icons的时候不会被其他的goroutine操作,但是这样做又会引发性能问题。

使用sync.Once改造的示例代码如下:

var icons map[string]image.Image

var loadIconsOnce sync.Once

func loadIcons() {
    icons = map[string]image.Image{
        "left":  loadIcon("left.png"),
        "up":    loadIcon("up.png"),
        "right": loadIcon("right.png"),
        "down":  loadIcon("down.png"),
    }
}

// Icon 是并发安全的
func Icon(name string) image.Image {
    loadIconsOnce.Do(loadIcons)
    return icorns[name]
}

并发安全的单例模式

下面是借助sync.Once实现的并发安全的单例模式:

package singleton

import (
	"sync"
)

type singleton struct {}

var instance *singleton {
    once.Do(func() {
        instance = &singleton{}
    })
    return instance
}

sync.Once其实内部包含一个互斥锁和一个布尔值,互斥锁保证布尔值和数据的安全,而布尔值用来记录初始化是否完成。这样设计就能保证初始化操作的时候是并发安全的并且初始化操作也不会被执行多次。

7.5、sync.Map

Go语言中内置的map不是并发安全的。示例:

var m = make(map[string]int)

func get(key string) int {
    return m[key]
}

func set(key string value int) {
    m[key] = value
}

func main() {
    wg := sync.WaitGroup{}
    for i := 0; i < 20; i++ {
        wg.Add(1)
        go func(n int) {
            key := strconv.Itoa(n)
            set(key, n)
            fmt.Printf("k=:%v,v:=%v\n", key, get(key))
            wg.Done()
        }(i)
    }
    wg.Wait()
}

上面的代码开启量在几个goroutine的时候可能没什么问题,当并发多了之后执行上面的代码就会报fatal error: concurrent map writes错误。

像这种场景下就需要为map加锁来保证并发的安全性了,Go语言的sync包中提供了一个开箱即用的并发安全版map-sync.Map。开箱即用表示不用像内置的map一样使用make函数初始化就能直接使用。同时sync.Map内置了诸如StoreLoadLoadOrStoreDeleteRange等操作方法。

var m = sync.Map{}
func main() {
    wg := sync.WaitGroup{}
    for i:=0;i<20;i++ {
        wg.Add(1)
        go func(n int) {
            key := strconv.Itoa(n)
            m.Store(key, n)
            value,_ := m.Load(key)
            fmt.Printf("k=:%v,v:=%v\n", key, value)
            wg.Done()
        }(i)
    }
    wg.Wait()
}

8、原子操作

在上面的代码中是通过锁操作来实现同步。而锁机制的底层是基于原子操作的,其一般直接通过CPU指令实现。Go语言中原子操作由内置的标准库sync/atomic提供。

atomic包

方法 解释
func LoadInt32(addr *int32) (val int32)
func LoadInt64(addr *int64) (val int64)
func LoadUint32(addr *uint32) (val uint32)
func LoadUint64(addr *uint64) (val uint64)
func LoadUintptr(addr *uintptr) (val uintptr)
func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)
读取操作
func StoreInt32(addr *int32, val int32)
func StoreInt64(addr *int64, val int64)
func StoreUint32(addr *uint32, val uint32)
func StoreUint64(addr *uint64, val uint64)
func StoreUintptr(addr *uintptr, val uintptr)
func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)
写入操作
func AddInt32(addr *int32, delta int32) (new int32)
func AddInt64(addr *int64, delta int64) (new int64)
func AddUint32(addr *uint32, delta uint32) (new uint32)
func AddUint64(addr *uint64, delta uint64) (new uint64)
func AddUintptr(addr *uintptr, delta uintptr) (new uintptr)
修改操作
func SwapInt32(addr *int32, new int32) (old int32)
func SwapInt64(addr *int64, new int64) (old int64)
func SwapUint32(addr *uint32, new uint32) (old uint32)
func SwapUint64(addr *uint64, new uint64) (old uint64)
func SwapUintptr(addr *uintptr, new uintptr) (old uintptr)
func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)
交换操作
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)
func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool)
func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool)
func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)
比较并交换操作

示例

比较互斥锁和原子操作的性能。

package main

import (
   "fmt"
   "sync"
   "sync/atomic"
   "time"
)

type Counter interface {
   Inc()
   Load() int64
}

// 普通版
type CommonCounter struct {
   counter int64
}

func (c CommonCounter) Inc() {
   c.counter++
}

func (c CommonCounter) Load() int64 {
   return c.counter
}

// 互斥锁版
type MutexCounter struct {
   counter int64
   lock    sync.Mutex
}

func (m *MutexCounter) Ico() {
   m.lock.Lock()
   defer m.lock.Unlock()
   m.counter++
}

func (m *MutexCounter) Load() int64 {
   m.lock.Lock()
   defer m.lock.Unlock()
   return m.counter
}

// 原子版操作
type AtomicCounter struct {
   counter int64
}

func (a *AtomicCounter) Inc() {
   atomic.AddInt64(&a.counter, 1)
}

func (a *AtomicCounter) Load() int64 {
   return atomic.LoadInt64(&a.counter)
}

func test(c Counter) {
   var wg sync.WaitGroup
   start := time.Now()
   for i:=0;i<1000;i++ {
       wg.Add(1)
       go func() {
           c.Inc()
           wg.Done()
       }()
   }
   wg.Wait()
   end := time.Now()
   fmt.Println(c.load(), end.Sub(start))
}

func main() {
   c1 := CommonCounter{} // 非并发安全
   test(c1)
   c2 := MutexCounter{} // 使用互斥锁实现并发安全
   test(&c2)
   c3 := AtomicCounter{} // 并发安全且比互斥锁效率高
   test(&c3)
}

atomic包提供了底层的原子级内存操作,对于同步算法的实现很有用。这些函数必须谨慎地保证正确使用。除了某些特殊的底层应用,使用通道或者sync包的函数/类型实现同步更好。

posted @ 2020-12-28 23:57  HashFlag  阅读(971)  评论(1编辑  收藏  举报