首页 > 其他分享 >第一章:基础知识

第一章:基础知识

时间:2024-03-14 23:11:27浏览次数:14  
标签:case 基础知识 int fmt 第一章 func Println main

1 五种变量创建的方法

Go 语言是静态类型语言,由于编译时,编译器会检查变量的类型,所以要求所有的变量都要有明确的类型。
变量在使用前,需要先声明。声明类型,就约定了你这个变量只能赋该类型的值。
声明一般有以下四种方法,其中前面两种同样也可用于定义常量,只需把关键字 var 变成 const 即可。

第一种 :一行声明一个变量

var <name> <type>

其中 var 是关键字(固定不变),name 是变量名,type 是类型。
使用 var ,虽然只指定了类型,但是 Go 会对其进行隐式初始化,比如 string 类型就初始化为空字符串,int 类型就初始化为0,float 就初始化为 0.0,bool类型就初始化为false,指针类型就初始化为 nil。
若想在声明过程,顺便也初始化,可以这样写

var name string = "Go"

在 Go 文件中的完整代码如下,为了不写重复性的代码,后续不再贴完整代码,只贴关键代码

package main

import "fmt"

func main()  {
    var name string = "Go"
    fmt.Println(name)
}

从右值(等号右边的值)来看,明显是个 string 类型(这里要注意,在 Python 双引号与单引号等价,但在 Go 中双引号和单引号是不一样的,这里要一定要使用双引号,表示字符串,而在单引号表示rune 类型的字符,这个后续会单独介绍),因此也可以将其简化为

var name = "Go"

若你的右值带有小数点,在不指定类型的情况下,编译器会将你的这个变量声明为 float64,但是很多情况下,我们并不需要这么高的精度(占用的内存空间更大)
这种情况下,推荐指定类型,不要偷懒

var rate float32 = 0.89

第二种:多个变量一起声明
声明多个变量,除了可以按照上面写成多行之外,还可以写成下面这样

var (
    name string
    age int
    gender string
)

第三种:声明和初始化一个变量
使用 := (推导声明写法或者短类型声明法:编译器会自动根据右值类型推断出左值的对应类型。),可以声明一个变量,并对其进行(显式)初始化。

name := "Go"

// 等价于

var name string = "Go"

// 等价于

var name = "Go"

但这种方法有个限制就是,只能用于函数内部

第四种:声明和初始化多个变量

name, age := "wangbm", 28

这种方法,也经常用于变量的交换

var a int = 100
var b int = 200
b, a = a, b

第五种:new 函数声明一个指针变量
在这里要先讲一下,指针的相关内容。
变量分为两种 普通变量 和 指针变量
普通变量,存放的是数据本身,而指针变量存放的是数据的地址。
如下代码,age 是一个普通变量,存放的内容是 28,而 ptr 是 存放变量age值的内存地址:0xc000010098

package main

import "fmt"

func main()  {
    var age int = 28
    var ptr = &age  // &后面接变量名,表示取出该变量的内存地址
    fmt.Println("age: ", age)
    fmt.Println("ptr: ", ptr)
}

输出

age:  28
ptr:  0xc000010098

而这里要说的 new 函数,是 Go 里的一个内建函数。
使用表达式 new(Type) 将创建一个Type类型的匿名变量,初始化为Type类型的零值,然后返回变量地址,返回的指针类型为*Type。

package main

import "fmt"

func main()  {
    ptr := new(int)
    fmt.Println("ptr address: ", ptr)
    fmt.Println("ptr value: ", *ptr)  // * 后面接指针变量,表示从内存地址中取出值
}

输出

ptr address:  0xc000010098
ptr value:  0

用new创建变量和普通变量声明语句方式创建变量没有什么区别,除了不需要声明一个临时变量的名字外,我们还可以在表达式中使用new(Type)。换言之,new函数类似是一种语法糖,而不是一个新的基础概念。
如下两种写法,可以说是等价的

// 使用 new
func newInt() *int {
    return new(int)
}

// 使用传统的方式
func newInt() *int {
    var dummy int
    return &dummy
}

以上不管哪种方法,变量/常量都只能声明一次,声明多次,编译就会报错。
但也有例外,这就要说到一个特殊变量:匿名变量,也称作占位符,或者空白标识符,用下划线表示。
匿名变量,优点有三:

  • 不分配内存,不占用内存空间
  • 不需要你为命名无用的变量名而纠结
  • 多次声明不会有任何问题

通常我们用匿名接收必须接收,但是又不会用到的值。

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

2 数据类型:整型与浮点型

2.1 整型

Go 语言中,整数类型可以再细分成10个类型,为了方便大家学习,我将这些类型整理成一张表格。
imagepng
int 和 uint 的区别就在于一个 u,有 u 说明是无符号,没有 u 代表有符号。

解释这个符号的区别
以 int8 和 uint8 举例,8 代表 8个bit,能表示的数值个数有 2^8 = 256。
uint8 是无符号,能表示的都是正数,0-255,刚好256个数。
int8 是有符号,既可以正数,也可以负数,那怎么办?对半分呗,-128-127,也刚好 256个数。
int8 int16 int32 int64 这几个类型的最后都有一个数值,这表明了它们能表示的数值个数是固定的。
而 int 并没有指定它的位数,说明它的大小,是可以变化的,那根据什么变化呢?

  • 当你在32位的系统下,int 和 uint 都占用 4个字节,也就是32位。
  • 若你在64位的系统下,int 和 uint 都占用 8个字节,也就是64位。

出于这个原因,在某些场景下,你应当避免使用 int 和 uint ,而使用更加精确的 int32 和 int64,比如在二进制传输、读写文件的结构描述(为了保持文件的结构不会受到不同编译目标平台字节长度的影响)

不同进制的表示方法
出于习惯,在初始化数据类型为整型的变量时,我们会使用10进制的表示法,因为它最直观,比如这样,表示整数10.

var num int = 10

不过,你要清楚,你一样可以使用其他进制来表示一个整数,这里以比较常用的2进制、8进制和16进制举例。
2进制:以0b或0B为前缀

var num01 int = 0b1100

8进制:以0o或者 0O为前缀

var num02 int = 0o14

16进制:以0x 为前缀

var num03 int = 0xC

下面用一段代码分别使用二进制、8进制、16进制来表示 10 进制的数值:12

package main

import (
    "fmt"
)

func main() {
    var num01 int = 0b1100
    var num02 int = 0o14
    var num03 int = 0xC

    fmt.Printf("2进制数 %b 表示的是: %d \n", num01, num01)
    fmt.Printf("8进制数 %o 表示的是: %d \n", num02, num02)
    fmt.Printf("16进制数 %X 表示的是: %d \n", num03, num03)
}

输出如下

2进制数 1100 表示的是: 12
8进制数 14 表示的是: 12
16进制数 C 表示的是: 12

以上代码用过了 fmt 包的格式化功能,你可以参考这里去看上面的代码

%b    表示为二进制
%c    该值对应的unicode码值
%d    表示为十进制
%o    表示为八进制
%q    该值对应的单引号括起来的go语法字符字面值,必要时会采用安全的转义表示
%x    表示为十六进制,使用a-f
%X    表示为十六进制,使用A-F
%U    表示为Unicode格式:U+1234,等价于"U+%04X"
%E    用科学计数法表示
%f    用浮点数表示

2.2 浮点型

浮点数类型的值一般由整数部分、小数点“.”和小数部分组成。
其中,整数部分和小数部分均由10进制表示法表示。不过还有另一种表示方法。那就是在其中加入指数部分。指数部分由“E”或“e”以及一个带正负号的10进制数组成。比如,3.7E-2表示浮点数0.037。又比如,3.7E+1表示浮点数37。
有时候,浮点数类型值的表示也可以被简化。比如,37.0可以被简化为37。又比如,0.037可以被简化为.037。
有一点需要注意,在Go语言里,浮点数的相关部分只能由10进制表示法表示,而不能由8进制表示法或16进制表示法表示。比如,03.7表示的一定是浮点数3.7。

float32 和 float64

Go语言中提供了两种精度的浮点数 float32 和 float64。

float32,也即我们常说的单精度,存储占用4个字节,也即48=32位,其中1位用来符号,8位用来指数,剩下的23位表示尾数
float64,也即我们熟悉的双精度,存储占用8个字节,也即8
8=64位,其中1位用来符号,11位用来指数,剩下的52位表示尾数
imagepng
那么精度是什么意思?有效位有多少位?
精度主要取决于尾数部分的位数。
对于 float32(单精度)来说,表示尾数的为23位,除去全部为0的情况以外,最小为2-23,约等于1.19*10-7,所以float小数部分只能精确到后面6位,加上小数点前的一位,即有效数字为7位。
同理 float64(单精度)的尾数部分为 52位,最小为2-52,约为2.22*10-16,所以精确到小数点后15位,加上小数点前的一位,有效位数为16位。
通过以上,可以总结出以下几点:
一、float32 和 float64 可以表示的数值很多
浮点数类型的取值范围可以从很微小到很巨大。浮点数取值范围的极限值可以在 math 包中找到:

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

二、数值很大但精度有限
人家虽然能表示的数值很大,但精度位却没有那么大。

  • float32的精度只能提供大约6个十进制数(表示后科学计数法后,小数点后6位)的精度
  • float64的精度能提供大约15个十进制数(表示后科学计数法后,小数点后15位)的精度

这里的精度是什么意思呢?
比如 10000018这个数,用 float32 的类型来表示的话,由于其有效位是7位,将10000018 表示成科学计数法,就是 1.0000018 * 10^7,能精确到小数点后面6位。
此时用科学计数法表示后,小数点后有7位,刚刚满足我们的精度要求,意思是什么呢?此时你对这个数进行+1或者-1等数学运算,都能保证计算结果是精确的

import "fmt"
var myfloat float32 = 10000018
func main()  {
    fmt.Println("myfloat: ", myfloat)
    fmt.Println("myfloat: ", myfloat+1)
}

输出如下

myfloat:  1.0000018e+07
myfloat:  1.0000019e+07

上面举了一个刚好满足精度要求数据的临界情况,为了做对比,下面也举一个刚好不满足精度要求的例子。只要给这个数值多加一位数就行了。
换成 100000187,同样使用 float32类型,表示成科学计数法,由于精度有限,表示的时候小数点后面7位是准确的,但若是对其进行数学运算,由于第八位无法表示,所以运算后第七位的值,就会变得不精确。
这里我们写个代码来验证一下,按照我们的理解下面 myfloat01 = 100000182 ,对其+5 操作后,应该等于 myfloat02 = 100000187,

import "fmt"

var myfloat01 float32 = 100000182
var myfloat02 float32 = 100000187

func main() {
    fmt.Println("myfloat: ", myfloat01)
    fmt.Println("myfloat: ", myfloat01+5)
    fmt.Println(myfloat02 == myfloat01+5)
}

但是由于其类型是 float32,精度不足,导致最后比较的结果是不相等(从小数点后第七位开始不精确)

myfloat:  1.00000184e+08
myfloat:  1.0000019e+08
false

由于精度的问题,就会出现这种很怪异的现象,myfloat == myfloat +1 会返回 true 。

3 数据类型:byte、rune与字符串

3.1 byte 与 rune

byte,占用1个节字,就 8 个比特位(2^8 = 256,因此 byte 的表示范围 0->255),所以它和 uint8 类型本质上没有区别,它表示的是 ACSII 表中的一个字符。
如下这段代码,分别定义了 byte 类型和 uint8 类型的变量 a 和 b

import "fmt"

func main() {
    var a byte = 65
    // 8进制写法: var a byte = '\101'     其中 \ 是固定前缀
    // 16进制写法: var a byte = '\x41'    其中 \x 是固定前缀

    var b uint8 = 66
    fmt.Printf("a 的值: %c \nb 的值: %c", a, b)

    // 或者使用 string 函数
    // fmt.Println("a 的值: ", string(a)," \nb 的值: ", string(b))
}

在 ASCII 表中,由于字母 A 的ASCII 的编号为 65 ,字母 B 的ASCII 编号为 66,所以上面的代码也可以写成这样

import "fmt"

func main() {
    var a byte = 'A'
    var b uint8 = 'B'
    fmt.Printf("a 的值: %c \nb 的值: %c", a, b)
}

他们的输出结果都是一样的。

a 的值: A
b 的值: B

rune,占用4个字节,共32位比特位,所以它和 int32 本质上也没有区别。它表示的是一个 Unicode字符(Unicode是一个可以表示世界范围内的绝大部分字符的编码规范)。

import (
    "fmt"
    "unsafe"
)

func main() {
    var a byte = 'A'
    var b rune = 'B'
    fmt.Printf("a 占用 %d 个字节数\nb 占用 %d 个字节数", unsafe.Sizeof(a), unsafe.Sizeof(b))
}

输出如下

a 占用 1 个字节数
b 占用 4 个字节数

由于 byte 类型能表示的值是有限,只有 2^8=256 个。所以如果你想表示中文的话,你只能使用 rune 类型。

var name rune = '中'

或许你已经发现,上面我们在定义字符时,不管是 byte 还是 rune ,我都是使用单引号,而没使用双引号。
对于从 Python 转过来的人,这里一定要注意了,在 Go 中单引号与 双引号并不是等价的。
单引号用来表示字符,在上面的例子里,如果你使用双引号,就意味着你要定义一个字符串,赋值时与前面声明的会不一致,这样在编译的时候就会出错。

cannot use "A" (type string) as type byte in assignment

上面我说了,byte 和 uint8 没有区别,rune 和 int32 没有区别,那为什么还要多出一个 byte 和 rune 类型呢?
理由很简单,因为uint8 和 int32 ,直观上让人以为这是一个数值,但是实际上,它也可以表示一个字符,所以为了消除这种直观错觉,就诞生了 byte 和 rune 这两个别名类型。

为什么rune是int32而不是uint32,解释如下:
https://stackoverflow.com/questions/24714665/why-is-rune-in-golang-an-alias-for-int32-and-not-uint32

3.2 字符串

字符串,可以说是大家很熟悉的数据类型之一。定义方法很简单

var mystr string = "hello"

上面说的byte 和 rune 都是字符类型,若多个字符放在一起,就组成了字符串,也就是这里要说的 string 类型。
比如 hello ,对照 ascii 编码表,每个字母对应的编号是:104,101,108,108,111

import (
    "fmt"
)

func main() {
    var mystr01 string = "hello"
    var mystr02 [5]byte = [5]byte{104, 101, 108, 108, 111}
    fmt.Printf("mystr01: %s\n", mystr01)
    fmt.Printf("mystr02: %s", mystr02)
}

输出如下,mystr01 和 mystr02 输出一样,说明了 string 的本质,其实是一个 byte数组

mystr01: hello
mystr02: hello

通过以上学习,我们知道字符分为 byte 和 rune,占用的大小不同。
这里来考一下大家,hello,中国 占用几个字节?
要回答这个问题,你得知道 Go 语言的 string 是用 uft-8 进行编码的,英文字母占用一个字节,而中文字母占用 3个字节,所以 hello,中国 的长度为 5+1+(3*2)= 12个字节。

import (
    "fmt"
)

func main() {
    var country string = "hello,中国"
    fmt.Println(len(country))
}
// 输出
12

以上虽然我都用双引号表示 一个字符串,但这并不是字符串的唯一表示方式。
除了双引号之外 ,你还可以使用反引号。
大多情况下,二者并没有区别,但如果你的字符串中有转义字符\ ,这里就要注意了,它们是有区别的。
使用反引号包裹的字符串,相当于 Python 中的 raw 字符串,会忽略里面的转义。
比如我想表示 \r\n 这个 字符串,使用双引号是这样写的,这种叫解释型表示法

var mystr01 string = "\\r\\n"

而使用反引号,就方便多了,所见即所得,这种叫原生型表示法

var mystr02 string = `\r\n`

他们的打印结果 都是一样的

import (
    "fmt"
)

func main() {
    var mystr01 string = "\\r\\n"
    var mystr02 string = `\r\n`
    fmt.Println(mystr01)
    fmt.Println(mystr02)
}

// output
\r\n
\r\n

如果你仍然想使用解释型的字符串,但是各种转义实在太麻烦了。你可以使用 fmt 的 %q 来还原一下。

import (
    "fmt"
)

func main() {
    var mystr01 string = `\r\n`
    fmt.Print(`\r\n`)
    fmt.Printf("的解释型字符串是: %q", mystr01)
}

输出如下

\r\n的解释型字符串是: "\\r\\n"

同时反引号可以不写换行符(因为没法写)来表示一个多行的字符串。

import (
    "fmt"
)

func main() {
    var mystr01 string = `你好呀!
hello world`

    fmt.Println(mystr01)
}

输出如下

你好呀!
hello world

4 数据类型:数组与切片

4.1 数组

数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。因为数组的长度是固定的,所以在Go语言中很少直接使用数组。
声明数组,并给该数组里的每个元素赋值(索引值的最小有效值和其他大多数语言一样是 0,不是1)

// [3] 里的3 表示该数组的元素个数及容量
var arr [3]int
arr[0] = 1
arr[1] = 2
arr[2] = 3

声明并直接初始化数组

// 第一种方法
var arr [3]int = [3]int{1,2,3}

// 第二种方法
arr := [3]int{1,2,3}

上面的 3 表示数组的元素个数 ,万一你哪天想往该数组中增加元素,你得对应修改这个数字,为了避免这种硬编码,你可以这样写,使用 ... 让Go语言自己根据实际情况来分配空间。

arr := [...]int{1,2,3}

[3]int 和 [4]int 虽然都是数组,但他们却是不同的类型,使用 fmt 的 %T 可以查得。

import (
    "fmt"
)

func main() {
    arr01 := [...]int{1, 2, 3}
    arr02 := [...]int{1, 2, 3, 4}
    fmt.Printf("%d 的类型是: %T\n", arr01, arr01)
    fmt.Printf("%d 的类型是: %T", arr02, arr02)
}

输出 如下

[1 2 3] 的类型是: [3]int
[1 2 3 4] 的类型是: [4]int

如果你觉得每次写 [3]int 有点麻烦,你可以为 [3]int 定义一个类型字面量,也就是别名类型。
使用 type 关键字可以定义一个类型字面量,后面只要你想定义一个容器大小为3,元素类型为int的数组 ,都可以使用这个别名类型。

import (
    "fmt"
)

func main() {
    type arr3 [3]int

    myarr := arr3{1,2,3}
    fmt.Printf("%d 的类型是: %T", myarr, myarr)
}

输出 如下

[1 2 3] 的类型是: main.arr3

其实定义数组还有一种偷懒的方法,比如下面这行代码

arr:=[4]int{2:3}

打印 arr,会是

[0 0 3 0]

可以看出[4]int{2:3},4表示数组有4个元素,2 和 3 分别表示该数组索引为2(初始索引为0)的值为3,而其他没有指定值的,就是 int 类型的零值,即0。

4.2 切片

切片(Slice)与数组一样,也是可以容纳若干类型相同的元素的容器。与数组不同的是,无法通过切片类型来确定其值的长度。每个切片值都会将数组作为其底层数据结构。我们也把这样的数组称为切片的底层数组。
切片是对数组的一个连续片段的引用,所以切片是一个引用类型,这个片段可以是整个数组,也可以是由起始和终止索引标识的一些项的子集,需要注意的是,终止索引标识的项不包括在切片内(意思是这是个左闭右开的区间)

import (
    "fmt"
)

func main() {
    myarr := []int{1, 2, 3}
    fmt.Printf("%d 的类型是: %T", myarr[0:2], myarr[0:2])
}

输出 如下

[1 2] 的类型是: []int

切片的构造,有四种方式

  1. 对数组进行片段截取,主要有如下两种写法
// 定义一个数组
myarr := [5]int{1,2,3,4,5}

// 【第一种】
// 1 表示从索引1开始,直到到索引为 2 (3-1)的元素
mysli1 := myarr[1:3]

// 【第二种】
// 1 表示从索引1开始,直到到索引为 2 (3-1)的元素
mysli2 := myarr[1:3:4]

如果你把上面的 mysli1 和 mysli2 打印出来,会发现他们居然是一样的。那第二种的 myarr[1:3:4] 的 4有什么用呢?在切片时,若不指定第三个数,那么切片终止索引会一直到原数组的最后一个数。而如果指定了第三个数,那么切片终止索引只会到原数组的该索引值。用下面这段代码来验证一下

package main

import "fmt"

func main(){
myarr := [5]int{1,2,3,4,5}
fmt.Printf("myarr 的长度为:%d,容量为:%d\n", len(myarr), cap(myarr))

mysli1 := myarr[1:3]
fmt.Printf("mysli1 的长度为:%d,容量为:%d\n", len(mysli1), cap(mysli1))
fmt.Println(mysli1)

mysli2 := myarr[1:3:4]
fmt.Printf("mysli2 的长度为:%d,容量为:%d\n", len(mysli2), cap(mysli2))
fmt.Println(mysli2)

}

输出如下,说明切片的第三个数,影响的只是切片的容量,而不会影响长度

myarr 的长度为:5,容量为:5
mysli1 的长度为:2,容量为:4
[2 3]
mysli2 的长度为:2,容量为:3
[2 3]

2. 从头声明赋值(例子如下)

// 声明字符串切片
var strList []string

// 声明整型切片
var numList []int

// 声明一个空切片
var numListEmpty = []int{}

3. 使用 make 函数构造,make 函数的格式:make( []Type, size, cap )这个函数刚好指出了,一个切片具备的三个要素:类型(Type),长度(size),容量(cap)

import (
"fmt"
)

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

输出 如下

[0 0] [0 0]
2 2
2 10

4. 使用和数组一样,偷懒的方法

import (
"fmt"
)

func main() {
a := []int{4:2}
fmt.Println(a)
fmt.Println(len(a), cap(a))
}

输出如下

[0 0 0 0 2]
5 5

关于 len 和 cap 的概念,可能不好理解 ,这里举个例子:

- 公司名,相当于字面量,也就是变量名。
- 公司里的所有工位,相当于已分配到的内存空间
- 公司里的员工,相当于元素。
- cap 代表你这个公司最多可以容纳多少员工
- len 代表你这个公司当前有多少个员工

由于 切片是引用类型,所以你不对它进行赋值的话,它的零值(默认值)是 nil

var myarr []int
fmt.Println(myarr == nil)
// true

数组 与 切片 有相同点,它们都是可以容纳若干类型相同的元素的容器
也有不同点,数组的容器大小固定,而切片本身是引用类型,它更像是 Python 中的 list ,我们可以对它 append 进行元素的添加。

import (
"fmt"
)

func main() {
myarr := []int{1}
// 追加一个元素
myarr = append(myarr, 2)
// 追加多个元素
myarr = append(myarr, 3, 4)
// 追加一个切片, ... 表示解包,不能省略
myarr = append(myarr, []int{7, 8}...)
// 在第一个位置插入元素
myarr = append([]int{0}, myarr...)
// 在中间插入一个切片(两个元素)
myarr = append(myarr[:5], append([]int{5,6}, myarr[5:]...)...)
fmt.Println(myarr)
}

输出 如下

[0 1 2 3 4 5 6 7 8]

## 4.3 思考题
最后,给你留道思考题。

package main

import (
"fmt"
)

func main() {
var numbers4 = [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
myslice := numbers4[4:6:8]
fmt.Printf("myslice为 %d, 其长度为: %d\n", myslice, len(myslice))

myslice = myslice[:cap(myslice)]
fmt.Printf("myslice的第四个元素为: %d", myslice[3])

}

为什么 myslice 的长度为2,却能访问到第四个元素

myslice为 [5 6], 其长度为: 2
myslice的第四个元素为: 8

答案

因为切片类型为指针类型,实际底层元素是引用数组,因为numbers4[4:6:8]引用的实际是[5, 6, 7, 8],所以可以访问到8,但是访问不到9,访问的时候会报index out of range

# 5 数据类型:字典与布尔类型
## 1. 字典
字典(Map 类型),是由若干个 key:value 这样的键值对映射组合在一起的数据结构。
它是哈希表的一个实现,这就要求它的每个映射里的key,都是唯一的,可以使用 == 和 != 来进行判等操作,换句话说就是key必须是可哈希的。
什么叫可哈希的?简单来说,一个不可变对象,都可以用一个哈希值来唯一表示,这样的不可变对象,比如字符串类型的对象(可以说除了切片、 字典,函数之外的其他内建类型都算)。
意思就是,你的 key 不能是切片,不能是字典,不能是函数。。
字典由key和value组成,它们各自有各自的类型。
在声明字典时,必须指定好你的key和value是什么类型的,然后使用 map 关键字来告诉Go这是一个字典。

map[KEY_TYPE]VALUE_TYPE

### 声明初始化字典
三种声明并初始化字典的方法

// 第一种方法
var scores map[string]int = map[string]int{"english": 80, "chinese": 85}

// 第二种方法
scores := map[string]int{"english": 80, "chinese": 85}

// 第三种方法
scores := make(map[string]int)
scores["english"] = 80
scores["chinese"] = 85

要注意的是,第一种方法如果拆分成多步(声明、初始化、再赋值),和其他两种有很大的不一样了,相对会比较麻烦。

import "fmt"

func main() {
// 声明一个名为 score 的字典
var scores map[string]int

// 未初始化的 score 的零值为nil,无法直接进行赋值
if scores == nil {
    // 需要使用 make 函数先对其初始化
    scores = make(map[string]int)
}

// 经过初始化后,就可以直接赋值
scores["chinese"] = 90
fmt.Println(scores)

}

### **字典的相关操作**
添加元素

scores["math"] = 95

更新元素,若key已存在,则直接更新value

scores["math"] = 100

读取元素,直接使用 [key] 即可 ,如果 key 不存在,也不报错,会返回其value-type 的零值。

fmt.Println(scores["math"])

删除元素,使用 delete 函数,如果 key 不存在,delete 函数会静默处理,不会报错。

delete(scores, "math")

当访问一个不存在的key时,并不会直接报错,而是会返回这个 value 的零值,如果 value的类型是int,就返回0。

package main

import "fmt"

func main() {
scores := make(map[string]int)
fmt.Println(scores["english"]) // 输出 0
}

### 判断 key 是否存在
当key不存在,会返回value-type的零值 ,所以你不能通过返回的结果是否是零值来判断对应的 key 是否存在,因为 key 对应的 value 值可能恰好就是零值。
其实字典的下标读取可以返回两个值,使用第二个返回值都表示对应的 key 是否存在,若存在ok为true,若不存在,则ok为false

import "fmt"

func main() {
scores := map[string]int{"english": 80, "chinese": 85}
math, ok := scores["math"]
if ok {
fmt.Printf("math 的值是: %d", math)
} else {
fmt.Println("math 不存在")
}
}

我们将上面的代码再优化一下

import "fmt"

func main() {
scores := map[string]int{"english": 80, "chinese": 85}
if math, ok := scores["math"]; ok {
fmt.Printf("math 的值是: %d", math)
} else {
fmt.Println("math 不存在")
}
}

### **如何对字典进行循环**
Go 语言中没有提供类似 Python 的 keys() 和 values() 这样方便的函数,想要获取,你得自己循环。
循环还分三种

1. 获取 key 和 value

import "fmt"

func main() {
scores := map[string]int{"english": 80, "chinese": 85}

for subject, score := range scores {
    fmt.Printf("key: %s, value: %d\n", subject, score)
}

}

2. 只获取key,这里注意不用占用符。

import "fmt"

func main() {
scores := map[string]int{"english": 80, "chinese": 85}

for subject := range scores {
    fmt.Printf("key: %s\n", subject)
}

}

3. 只获取 value,用一个占位符替代。

import "fmt"

func main() {
scores := map[string]int{"english": 80, "chinese": 85}

for _, score := range scores {
    fmt.Printf("value: %d\n", score)
}

}

## 2. 布尔类型
关于布尔值,无非就两个值:true 和 false。只是这两个值,在不同的语言里可能不同。
在 Python 中,真值用 True 表示,与 1 相等,假值用 False 表示,与 0 相等
```python
>>> True == 1
True
>>> False == 0
True
>>>

而在 Go 中,真值用 true 表示,不但不与 1 相等,并且更加严格,不同类型无法进行比较,而假值用 false 表示,同样与 0 无法比较。
bool 与 int 不能直接转换,如果要转换,需要你自己实现函数。
bool 转 int

func bool2int(b bool) int {
    if b {
        return 1
    }
    return 0
}

int 转 bool

func int2bool(i int) bool {
    return i != 0
}

在 Python 中使用 not 对逻辑值取反,而 Go 中使用 ! 符号

import "fmt"

var male bool = true
func main()  {
    fmt.Println( !male == false)
    // 或者
    fmt.Println( male != false)
}

// output: true

一个 if 判断语句,有可能不只一个判断条件,在 Python 中是使用 and 和 or 来执行逻辑运算

>>> age = 15
>>> gender = "male"
>>>
>>> gender == "male" and age >18
False

而在 Go 语言中,则使用 && 表示且,用 || 表示或,并且有短路行为(即左边表达式已经可以确认整个表达式的值,那么右边将不会再被求值。

import "fmt"

var age int = 15
var gender string = "male"
func main()  {
    //  && 两边的表达式都会执行
    fmt.Println( gender == "male" && age > 18)
    // gender == "male" 并不会执行
    fmt.Println( age < 18 || gender == "male")
}

// output: false
// output: true

6 数据类型:指针

6.1 什么是指针

当我们定义一个变量 name

var name string = "Go"

此时,name 是变量名,它只是编程语言中方便程序员编写和理解代码的一个标签。
当我们访问这个标签时,机算机会返回给我们它指向的内存地址里存储的值:Go。
出于某些需要,我们会将这个内存地址赋值给另一个变量名,通常叫做 ptr(pointer的简写),而这个变量,我们称之为指针变量。
换句话说,指针变量(一个标签)的值是指针,也就是内存地址。
根据变量指向的值,是否是内存地址,我把变量分为两种:

  • 普通变量:存数据值本身

  • 指针变量:存值的内存地址

    6.2 指针的创建

    指针创建有三种方法
    第一种方法
    先定义对应的变量,再通过变量取得内存地址,创建指针

    // 定义普通变量
    aint := 1
    // 定义指针变量
    ptr := &aint
    

    第二种方法
    先创建指针,分配好内存后,再给指针指向的内存地址写入对应的值。

    // 创建指针
    astr := new(string)
    // 给指针赋值
    *astr = "Go编程时光"
    

    第三种方法
    先声明一个指针变量,再从其他变量取得内存地址赋值给它

    aint := 1
    var bint *int  // 声明一个指针
    bint = &aint   // 初始化
    

    上面的三段代码中,指针的操作都离不开这两个符号:

  • & :从一个普通变量中取得内存地址

  • :当在赋值操作符(=)的右边,是从一个指针变量中取得变量值,当*在赋值操作符(=)的左边,是指该指针指向的变量

通过下面这段代码,你可以熟悉这两个符号的用法

package main

import "fmt"

func main() {
    aint := 1     // 定义普通变量
    ptr := &aint  // 定义指针变量
    fmt.Println("普通变量存储的是:", aint)
    fmt.Println("普通变量存储的是:", *ptr)
    fmt.Println("指针变量存储的是:", &aint)
    fmt.Println("指针变量存储的是:", ptr)
}

输出如下

普通变量存储的是: 1
普通变量存储的是: 1
指针变量存储的是: 0xc0000100a0
指针变量存储的是: 0xc0000100a0

要想打印指针指向的内存地址,方法有两种

// 第一种
fmt.Printf("%p", ptr)

// 第二种
fmt.Println(ptr)

6.3 指针的类型

我们知道字符串的类型是 string,整型是int,那么指针如何表示呢?
写段代码试验一下就知道了

package main

import "fmt"

func main() {
    astr := "hello"
    aint := 1
    abool := false
    arune := 'a'
    afloat := 1.2

    fmt.Printf("astr 指针类型是:%T\n", &astr)
    fmt.Printf("aint 指针类型是:%T\n", &aint)
    fmt.Printf("abool 指针类型是:%T\n", &abool)
    fmt.Printf("arune 指针类型是:%T\n", &arune)
    fmt.Printf("afloat 指针类型是:%T\n", &afloat)
}

输出如下,可以发现用 *+所指向变量值的数据类型,就是对应的指针类型。

astr 指针类型是:*string
aint 指针类型是:*int
abool 指针类型是:*bool
arune 指针类型是:*int32
afloat 指针类型是:*float64

所以若我们定义一个只接收指针类型的参数的函数,可以这么写

func mytest(ptr *int)  {
    fmt.Println(*ptr)
}

6.4 指针的零值

当指针声明后,没有进行初始化,其零值是 nil。

func main() {
    a := 25
    var b *int  // 声明一个指针

    if b == nil {
        fmt.Println(b)
        b = &a  // 初始化:将a的内存地址给b
        fmt.Println(b)
    }
}

输出如下

<nil>
0xc0000100a0

4. 指针与切片

切片与指针一样,都是引用类型。
如果我们想通过一个函数改变一个数组的值,有两种方法

  1. 将这个数组的切片做为参数传给函数
  2. 将这个数组的指针做为参数传给函数

尽管二者都可以实现我们的目的,但是按照 Go 语言的使用习惯,建议使用第一种方法,因为第一种方法,写出来的代码会更加简洁,易读。具体你可以参数下面两种方法的代码实现
使用切片

func modify(sls []int) {
    sls[0] = 90
}

func main() {
    a := [3]int{89, 90, 91}
    modify(a[:])
    fmt.Println(a)
}

使用指针

func modify(arr *[3]int) {
    (*arr)[0] = 90
}

func main() {
    a := [3]int{89, 90, 91}
    modify(&a)
    fmt.Println(a)
}

7 流程控制:if-else

7.1 条件语句模型

Go里的流程控制方法还是挺丰富,整理了下有如下这么多种:

  • if - else 条件语句
  • switch - case 选择语句
  • for - range 循环语句
  • goto 无条件跳转语句
  • defer 延迟执行

今天先来讲讲 if-else 条件语句
Go 里的条件语句模型是这样的
Go编译器,对于 { 和 } 的位置有严格的要求,它要求 else if (或 else)和 两边的花括号,必须在同一行。
由于 Go是 强类型,所以要求你条件表达式必须严格返回布尔型的数据(nil 和 0 和 1 都不行,具体可查看《详解数据类型:字典与布尔类型》)。
对于这个模型,分别举几个例子来看一下。

if 条件 1 {
  分支 1
} else if 条件 2 {
  分支 2
} else if 条件 ... {
  分支 ...
} else {
  分支 else
}

7.2 单分支判断

只有一个 if ,没有 else

import "fmt"

func main() {
    age := 20
    if age > 18 {
        fmt.Println("已经成年了")
    }
}

如果条件里需要满足多个条件,可以使用 && 和 ||

  • &&:表示且,左右都需要为true,最终结果才能为 true,否则为 false

  • ||:表示或,左右只要有一个为true,最终结果即为true,否则 为 false

    import "fmt"
    

func main() {
age := 20
gender := "male"
if (age > 18 && gender == "male") {
fmt.Println("是成年男性")
}
}

## 7.3 多分支判断
if - else

import "fmt"

func main() {
age := 20
if age > 18 {
fmt.Println("已经成年了")
} else {
fmt.Println("还未成年")
}
}

if - else if - else

import "fmt"

func main() {
age := 20
if age > 18 {
fmt.Println("已经成年了")
} else if age >12 {
fmt.Println("已经是青少年了")
} else {
fmt.Println("还不是青少年")
}
}

## 7.4 高级写法
在 if 里可以允许先运行一个表达式,取得变量后,再对其进行判断,比如第一个例子里代码也可以写成这样

import "fmt"

func main() {
if age := 20;age > 18 {
fmt.Println("已经成年了")
}
}

# 8 流程控制:switch-case
Go里的流程控制方法还是挺丰富,整理了下有如下这么多种:

- if - else 条件语句
- switch - case 选择语句
- for - range 循环语句
- goto 无条件跳转语句
- defer 延迟执行

上一篇讲了 if -else 条件语句,今天先来讲讲 switch - case 选择语句。
## 8.1 语句模型
Go 里的选择语句模型是这样的
拿 switch 后的表达式分别和 case 后的表达式进行对比,只要有一个 case 满足条件,就会执行对应的代码块,然后直接退出 switch - case ,如果 一个都没有满足,才会执行 default 的代码块。

switch 表达式 {
case 表达式1:
代码块
case 表达式2:
代码块
case 表达式3:
代码块
case 表达式4:
代码块
case 表达式5:
代码块
default:
代码块
}

## 8.2 最简单的示例
switch 后接一个你要判断变量 education (学历),然后 case 会拿这个 变量去和它后面的表达式(可能是常量、变量、表达式等)进行判等。
如果相等,就执行相应的代码块。如果不相等,就接着下一个 case。

import "fmt"

func main() {
education := "本科"

switch education {
case "博士":
    fmt.Println("我是博士")
case "研究生":
    fmt.Println("我是研究生")
case "本科":
    fmt.Println("我是本科生")
case "大专":
    fmt.Println("我是大专生")
case "高中":
    fmt.Println("我是高中生")
default:
    fmt.Println("学历未达标..")
}

}

输出如下

我是本科生

## 8.3 一个 case 多个条件
case 后可以接多个多个条件,多个条件之间是 或 的关系,用逗号相隔。

import "fmt"

func main() {
month := 2

switch month {
case 3, 4, 5:
    fmt.Println("春天")
case 6, 7, 8:
    fmt.Println("夏天")
case 9, 10, 11:
    fmt.Println("秋天")
case 12, 1, 2:
    fmt.Println("冬天")
default:
    fmt.Println("输入有误...")
}

}

输出如下

冬天

## 8.4 case 条件常量不能重复
当 case 后接的是常量时,该常量只能出现一次。
以下两种情况,在编译时,都会报错: duplicate case “male” in switch
**错误案例一**

gender := "male"

switch gender {
case "male":
fmt.Println("男性")
// 与上面重复
case "male":
fmt.Println("男性")
case "female":
fmt.Println("女性")
}

**错误案例二**

gender := "male"

switch gender {
case "male", "male":
fmt.Println("男性")
case "female":
fmt.Println("女性")
}

## 8.5 switch 后可接函数
switch 后面可以接一个函数,只要保证 case 后的值类型与函数的返回值 一致即可。

import "fmt"

// 判断一个同学是否有挂科记录的函数
// 返回值是布尔类型
func getResult(args ...int) bool {
for _, i := range args {
if i < 60 {
return false
}
}
return true
}

func main() {
chinese := 80
english := 50
math := 100

switch getResult(chinese, english, math) {
// case 后也必须 是布尔类型
case true:
    fmt.Println("该同学所有成绩都合格")
case false:
    fmt.Println("该同学有挂科记录")
}

}

## 8.6 switch 可不接表达式
switch 后可以不接任何变量、表达式、函数。
当不接任何东西时,switch - case 就相当于 if - elseif - else

score := 30

switch {
case score >= 95 && score <= 100:
fmt.Println("优秀")
case score >= 80:
fmt.Println("良好")
case score >= 60:
fmt.Println("合格")
case score >= 0:
fmt.Println("不合格")
default:
fmt.Println("输入有误...")
}

## 8.7 switch 的穿透能力
正常情况下 switch - case 的执行顺序是:只要有一个 case 满足条件,就会直接退出 switch - case ,如果 一个都没有满足,才会执行 default 的代码块。
但是有一种情况是例外。
那就是当 case 使用关键字 fallthrough 开启穿透能力的时候。

s := "hello"
switch {
case s == "hello":
fmt.Println("hello")
fallthrough
case s != "world":
fmt.Println("world")
}

代码输出如下:

hello
world

需要注意的是,fallthrough 只能穿透一层,意思是它让你直接执行下一个case的语句,而且不需要判断条件。

s := "hello"
switch {
case s == "hello":
fmt.Println("hello")
fallthrough
case s == "xxxx":
fmt.Println("xxxx")
case s != "world":
fmt.Println("world")
}

输出如下,并不会输出 world(即使它符合条件)

hello
xxxx

# 9 流程控制:for 循环
Go里的流程控制方法还是挺丰富,整理了下有如下这么多种:

- if - else 条件语句
- switch - case 选择语句
- for - range 循环语句
- goto 无条件跳转语句
- defer 延迟执行

上一篇讲了switch - case 选择语句,今天先来讲讲 for 循环语句。
## 9.1 语句模型
这是 for 循环的基本模型。

for [condition | ( init; condition; increment ) | Range]
{
statement(s);
}

可以看到 for 后面,可以接三种类型的表达式。

1. 接一个条件表达式
2. 接三个表达式
3. 接一个 range 表达式

但其实还有第四种

4. 不接表达式
## 9.2 接一个条件表达式
这个例子会打印 1 到 5 的数值。

a := 1
for a <= 5 {
fmt.Println(a)
a ++
}

输出如下

1
2
3
4
5

## 9.3 接三个表达式
for 后面,紧接着三个表达式,使用 ; 分隔。
这三个表达式,各有各的用途

- 第一个表达式:初始化控制变量,在整个循环生命周期内,只运行一次;
- 第二个表达式:设置循环控制条件,当返回true,继续循环,返回false,结束循环;
- 第三个表达式:每次循完开始(除第一次)时,给控制变量增量或减量。

这边的例子和上面的例子,是等价的。

import "fmt"

func main() {
for i := 1; i <= 5; i++ {
fmt.Println(i)
}
}

输出如下、

1
2
3
4
5

## 9.4 不接表达式:无限循环
在 Go 语言中,没有 while 循环,如果要实现无限循环,也完全可以 for 来实现。
当你不加任何的判断条件时, 就相当于你每次的判断都为 true,程序就会一直处于运行状态,但是一般我们并不会让程序处于死循环,在满足一定的条件下,可以使用关键字 break 退出循环体,也可以使用 continue 直接跳到下一循环。
下面两种写法都是无限循环的写法。

for {
代码块
}

// 等价于
for ;; {
代码块
}

举个例子

import "fmt"

func main() {
var i int = 1
for {
if i > 5 {
break
}
fmt.Printf("hello, %d\n", i)
i++
}
}

输出如下

hello, 1
hello, 2
hello, 3
hello, 4
hello, 5

## 9.5 接 for-range 语句
遍历一个可迭代对象,是一个很常用的操作。在 Go 可以使用 for-range 的方式来实现。
range 后可接数组、切片,字符串等
由于 range 会返回两个值:索引和数据,若你后面的代码用不到索引,需要使用 _ 表示 。

import "fmt"

func main() {
myarr := [...]string{"world", "python", "go"}
for _, item := range myarr {
fmt.Printf("hello, %s\n", item)
}
}

输出如下

hello, world
hello, python
hello, go

如果你用一个变量来接收的话,接收到的是索引

import "fmt"

func main() {
myarr := [...]string{"world", "python", "go"}
for i := range myarr {
fmt.Printf("hello, %v\n", i)
}
}

输出如下

hello, 0
hello, 1
hello, 2

# 10 流程控制:goto 无条件跳转
Go里的流程控制方法还是挺丰富,整理了下有如下这么多种:

- if - else 条件语句
- switch - case 选择语句
- for - range 循环语句
- goto 无条件跳转语句
- defer 延迟执行

前面三种,我已经都讲过了,今天要讲讲 goto 的无条件跳转。
很难想象在 Go 居然会保留 goto,因为很多人不建议使用 goto,所以在一些编程语言中甚至直接取消了 goto。
我感觉 Go 既然保留,一定有人家的理由,只是我目前还没感受到。不管怎样,咱还是照常学习吧。
## 10.1 基本模型
goto 顾言思义,是跳转的意思。
goto 后接一个标签,这个标签的意义是告诉 Go程序下一步要执行哪里的代码。
所以这个标签如何放置,放置在哪里,是 goto 里最需要注意的。

goto 标签;
...
...
标签: 表达式;

## 10.2 最简单的示例
goto 可以打破原有代码执行顺序,直接跳转到某一行执行代码。

import "fmt"

func main() {

goto flag
fmt.Println("B")

flag:
fmt.Println("A")

}

执行结果,并不会输出 B ,而只会输出 A

A

## 10.3 如何使用?
goto 语句通常与条件语句配合使用。可用来实现条件转移, 构成循环,跳出循环体等功能。
这边举一个例子,用 goto 的方式来实现一个打印 1到5 的循环。

import "fmt"

func main() {
i := 1
flag:
if i <= 5 {
fmt.Println(i)
i++
goto flag
}
}

输出如下

1
2
3
4
5

再举个例子,使用 goto 实现 类型 break 的效果。

import "fmt"

func main() {
i := 1
for {
if i > 5 {
goto flag
}
fmt.Println(i)
i++
}
flag:
}

输出如下

1
2
3
4
5

最后再举个例子,使用 goto 实现 类型 continue的效果,打印 1到10 的所有偶数。

import "fmt"

func main() {
i := 1
flag:
for i <= 10 {
if i%2 == 1 {
i++
goto flag
}
fmt.Println(i)
i++
}
}

输出如下

2
4
6
8
10

## 10.4 注意事项
goto语句与标签之间不能有变量声明,否则编译错误。

import "fmt"

func main() {
fmt.Println("start")
goto flag
var say = "hello oldboy"
fmt.Println(say)
flag:
fmt.Println("end")
}

编译错误

.\main.go:7:7: goto flag jumps over declaration of say at .\main.go:8:6

# 11 流程控制:defer 延迟语句
Go里的流程控制方法还是挺丰富,整理了下有如下这么多种:

- if - else 条件语句
- switch - case 选择语句
- for - range 循环语句
- goto 无条件跳转语句
- defer 延迟执行

今天是最后一篇讲控制流程了,内容是 defer 延迟语句,这个在其他编程语言里好像没有见到。应该是属于 Go 语言里的独有的关键字,但即使如此,阅读后这篇文章后,你可以发现 defer 在其他编程语言里的影子。
## 11.1 延迟调用
defer 的用法很简单,只要在后面跟一个函数的调用,就能实现将这个 xxx 函数的调用延迟到当前函数执行完后再执行。

defer xxx()

这是一个很简单的例子,可以很快帮助你理解 defer 的使用效果。

import "fmt"

func myfunc() {
fmt.Println("B")
}

func main() {
defer myfunc()
fmt.Println("A")
}

输出如下

A
B

当然了,对于上面这个例子可以简写为成如下,输出结果是一致的

import "fmt"

func main() {
defer fmt.Println("B")
fmt.Println("A")
}

## 11.2 即时求值的变量快照
使用 defer 只是延时调用函数,此时传递给函数里的变量,不应该受到后续程序的影响。
比如这边的例子

import "fmt"

func main() {
name := "go"
defer fmt.Println(name) // 输出: go

name = "python"
fmt.Println(name)      // 输出: python

}

输出如下,可见给 name 重新赋值为 python,后续调用 defer 的时候,仍然使用未重新赋值的变量值,就好在 defer 这里,给所有的这是做了一个快照一样。

python
go

如果 defer 后面跟的是匿名函数,情况会有所不同, defer 会取到最后的变量值

package main

import "fmt"

func main() {
name := "go"
defer func(){
fmt.Println(name) // 输出: python
}()
name = "python"
fmt.Println(name) // 输出: python
}

这种现象对于新手来说,确实是个难点,不过这是面试的热点话题
## 11.3 多个defer 反序调用
当我们在一个函数里使用了 多个defer,那么这些defer 的执行函数是如何的呢?
做个试验就知道了

import "fmt"

func main() {
name := "go"
defer fmt.Println(name) // 输出: go

name = "python"
defer fmt.Println(name) // 输出: python

name = "java"
fmt.Println(name)

}

输出如下,可见 多个defer 是反序调用的,有点类似栈一样,后进先出。

java
python
go

## 11.4 defer 与 return 孰先孰后
至此,defer 还算是挺好理解的。在一般的使用上,是没有问题了。
在这里提一个稍微复杂一点的问题,defer 和 return 到底是哪个先调用?
使用下面这段代码,可以很容易的观察出来

import "fmt"

var name string = "go"

func myfunc() string {
defer func() {
name = "python"
}()

fmt.Printf("myfunc 函数里的name:%s\n", name)
return name

}

func main() {
myname := myfunc()
fmt.Printf("main 函数里的name: %s\n", name)
fmt.Println("main 函数里的myname: ", myname)
}

输出如下

myfunc 函数里的name:go
main 函数里的name: python
main 函数里的myname: go

来一起理解一下这段代码,第一行很直观,name 此时还是全局变量,值还是go
第二行也不难理解,在 defer 里改变了这个全局变量,此时name的值已经变成了 python
重点在第三行,为什么输出的是 go ?
解释是return不是原子性操作,1先复制 2再调用return指令,而defer就在 这 1 2之间。
## 11.5 为什么要有 defer?
看完上面的例子后,不知道你是否和我一样,对这个defer的使用效果感到熟悉?貌似在 Python 也见过类似的用法。
虽然 Python 中没有 defer ,但是它有 with 上下文管理器。我们知道在 Python 中可以使用 defer 实现对资源的管理。最常用的例子就是文件的打开关闭。
你可能会有疑问,这也没什么意义呀,我把这个放在 defer 执行的函数放在 return 那里执行不就好了。
固然可以,但是当一个函数里有多个 return 时,你得多调用好多次这个函数,代码就臃肿起来了。
若是没有 defer,你可以写出这样的代码

func f() {
r := getResource() //0,获取资源
......
if ... {
r.release() //1,释放资源
return
}
......
if ... {
r.release() //2,释放资源
return
}
......
if ... {
r.release() //3,释放资源
return
}
......
r.release() //4,释放资源
return
}

使用了 defer 后,代码就显得简单直接,不管你在何处 return,都会执行 defer 后的函数。

func f() {
r := getResource() //0,获取资源

defer r.release()  //1,释放资源
......
if ... {
    ...
    return
}
......
if ... {
    ...
    return
}
......
if ... {
    ...
    return
}
......
return

}

# 12 流程控制:理解 select 用法
跟 switch-case 相比,select-case 用法比较单一,它仅能用于 信道/通道 的相关操作。

select {
case 表达式1:

case 表达式2:

default:

}

接下来,我们来看几个例子帮助理解这个 select 的模型。
## 12.1 最简单的例子
先创建两个信道,并在 select 前往 c2 发送数据

package main

import (
"fmt"
)

func main() {
c1 := make(chan string, 1)
c2 := make(chan string, 1)

c2 <- "hello"

select {
case msg1 := <-c1:
  fmt.Println("c1 received: ", msg1)
case msg2 := <-c2:
  fmt.Println("c2 received: ", msg2)
default:
  fmt.Println("No data received.")
}

}

在运行 select 时,会遍历所有(如果有机会的话)的 case 表达式,只要有一个信道有接收到数据,那么 select 就结束,所以输出如下

c2 received: hello

## 12.2 避免造成死锁
select 在执行过程中,必须命中其中的某一分支。
如果在遍历完所有的 case 后,若没有命中(命中:也许这样描述不太准确,我本意是想说可以执行信道的操作语句)任何一个 case 表达式,就会进入 default 里的代码分支。
但如果你没有写 default 分支,select 就会阻塞,直到有某个 case 可以命中,而如果一直没有命中,select 就会抛出 deadlock 的错误,就像下面这样子。

package main

import (
"fmt"
)

func main() {
c1 := make(chan string, 1)
c2 := make(chan string, 1)

// c2 <- "hello"

select {
case msg1 := <-c1:
    fmt.Println("c1 received: ", msg1)
case msg2 := <-c2:
    fmt.Println("c2 received: ", msg2)
    // default:
    //  fmt.Println("No data received.")
}

}

运行后输出如下

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [select]:
main.main()
/Users/MING/GolandProjects/golang-test/main.go:13 +0x10f
exit status 2

**解决这个问题的方法有两种**
一个是,养成好习惯,在 select 的时候,也写好 default 分支代码,尽管你 default 下没有写任何代码。

package main

import (
"fmt"
)

func main() {
c1 := make(chan string, 1)
c2 := make(chan string, 1)

// c2 <- "hello"

select {
case msg1 := <-c1:
    fmt.Println("c1 received: ", msg1)
case msg2 := <-c2:
    fmt.Println("c2 received: ", msg2)
default:

}

}

另一个是,让其中某一个信道可以接收到数据

package main

import (
"fmt"
"time"
)

func main() {
c1 := make(chan string, 1)
c2 := make(chan string, 1)

// 开启一个协程,可以发送数据到信道
go func() {
time.Sleep(time.Second * 1)
c2 <- "hello"
}()

select {
case msg1 := <-c1:
    fmt.Println("c1 received: ", msg1)
case msg2 := <-c2:
    fmt.Println("c2 received: ", msg2)
}

}

## 12.3 select 随机性
之前学过 switch 的时候,知道了 switch 里的 case 是顺序执行的,但在 select 里却不是。
通过下面这个例子的执行结果就可以看出
![image.png](https://cdn.nlark.com/yuque/0/2024/png/40402474/1710164920844-5fc883a8-29bf-42d0-a68b-681ea2b11dbd.png#averageHue=%23282a35&clientId=u0131433d-5d81-4&from=paste&id=ubaa188d3&originHeight=1244&originWidth=1252&originalType=url&ratio=1.25&rotation=0&showTitle=false&size=174114&status=done&style=none&taskId=u902cdb89-df4c-4231-bceb-c2e1602e08b&title=)
## 12. 4select 的超时
当 case 里的信道始终没有接收到数据时,而且也没有 default 语句时,select 整体就会阻塞,但是有时我们并不希望 select 一直阻塞下去,这时候就可以手动设置一个超时时间。

package main

import (
"fmt"
"time"
)

func makeTimeout(ch chan bool, t int) {
time.Sleep(time.Second * time.Duration(t))
ch <- true
}

func main() {
c1 := make(chan string, 1)
c2 := make(chan string, 1)
timeout := make(chan bool, 1)

go makeTimeout(timeout, 2)

select {
case msg1 := <-c1:
    fmt.Println("c1 received: ", msg1)
case msg2 := <-c2:
    fmt.Println("c2 received: ", msg2)
case <-timeout:
    fmt.Println("Timeout, exit.")
}

}

输出如下

Timeout, exit.

## 12.5 读取/写入都可以
上面例子里的 case,好像都只从信道中读取数据,但实际上,select 里的 case 表达式只要求你是对信道的操作即可,不管你是往信道写入数据,还是从信道读出数据。

package main

import (
"fmt"
)

func main() {
c1 := make(chan int, 2)

c1 <- 2
select {
case c1 <- 4:
    fmt.Println("c1 received: ", <-c1)
    fmt.Println("c1 received: ", <-c1)
default:
    fmt.Println("channel blocking")
}

}

输出如下

c1 received: 2
c1 received: 4

## 12.6 信道关闭也能命中
上面的例子基本都是信道有数据可读取、或者信道可写入数据。其实当一个信道被 close 后,select 也能命中。
举个例子

package main

import "fmt"

func main() {
c1 := make(chan int, 1)
c2 := make(chan int, 1)
close(c1)
for {
select {
case <-c1:
fmt.Println("stop")
return
case <-c2:
fmt.Println("hhh")

    }
}

}

执行 go run main.go 后,会立马输出 stop

$ go run main.go
stop

## 12.7 总结一下
select 与 switch 原理很相似,但它的使用场景更特殊,学习了本篇文章,你需要知道如下几点区别:

1. select 只能用于 channel 的操作(写入/读出/关闭),而 switch 则更通用一些;
2. select 的 case 是随机的,而 switch 里的 case 是顺序执行;
3. select 要注意避免出现死锁,同时也可以自行实现超时机制;
4. select 里没有类似 switch 里的 fallthrough 的用法;
5. select 不能像 switch 一样接函数或其他表达式。
# 13 异常机制:panic 和 recover
编程语言一般都会有异常捕获机制,在 Python 中 是使用raise 和 try-except 语句来实现的异常抛出和异常捕获的。
在 Golang 中,有不少常规错误,在编译阶段就能提前告警,比如语法错误或类型错误等,但是有些错误仅能在程序运行后才能发生,比如数组访问越界、空指针引用等,这些运行时错误会引起程序退出。
当然能触发程序宕机退出的,也可以是我们自己,比如经过检查判断,当前环境无法达到我们程序进行的预期条件时(比如一个服务指定监听端口被其他程序占用),可以手动触发 panic,让程序退出停止运行。
## 13.1 触发panic
手动触发宕机,是非常简单的一件事,只需要调用 panic 这个内置函数即可,就像这样子

package main

func main() {
panic("crash")
}

运行后,直接报错宕机

$ go run main.go
go run main.go
panic: crash

goroutine 1 [running]:
main.main()
E:/Go-Code/main.go:4 +0x40
exit status 2

## 13.2 捕获 panic
发生了异常,有时候就得捕获,就像 Python 中的except 一样,那 Golang 中是如何做到的呢?
这就不得不引出另外一个内建函数 – recover,它可以让程序在发生宕机后起生回生。
但是 recover 的使用,有一个条件,就是它必须在 defer 函数中才能生效,其他作用域下,它是不工作的。
这是一个简单的例子

import "fmt"

func set_data(x int) {
defer func() {
// recover() 可以将捕获到的panic信息打印
if err := recover(); err != nil {
fmt.Println(err)
}
}()

// 故意制造数组越界,触发 panic
var arr [10]int
arr[x] = 88

}

func main() {
set_data(20)

// 如果能执行到这句,说明panic被捕获了
// 后续的程序能继续运行
fmt.Println("everything is ok")

}

运行后,输出如下

$ go run main.go
runtime error: index out of range [20] with length 10
everything is ok

通常来说,不应该对进入 panic 宕机的程序做任何处理,但有时,需要我们可以从宕机中恢复,至少我们可以在程序崩溃前,做一些操作,举个例子,当 web 服务器遇到不可预料的严重问题时,在崩溃前应该将所有的连接关闭,如果不做任何处理,会使得客户端一直处于等待状态,如果 web 服务器还在开发阶段,服务器甚至可以将异常信息反馈到客户端,帮助调试。
## 13.3 无法跨协程
从上面的例子,可以看到,即使 panic 会导致整个程序退出,但在退出前,若有 defer 延迟函数,还是得执行完 defer 。
但是这个 defer 在多个协程之间是没有效果,在子协程里触发 panic,只能触发自己协程内的 defer,而不能调用 main 协程里的 defer 函数的。
来做个实验就知道了

import (
"fmt"
"time"
)

func main() {
// 这个 defer 并不会执行
defer fmt.Println("in main")

go func() {
    defer println("in goroutine")
    panic("")
}()

time.Sleep(2 * time.Second)

}

输出如下

in goroutine
panic:

goroutine 6 [running]:
main.main.func1()
E:/Go-Code/main.go:12 +0x7b
created by main.main
E:/Go-Code/main.go:10 +0xbc
exit status 2

## 13.4 总结一下
Golang 异常的抛出与捕获,依赖两个内置函数:

- panic:抛出异常,使程序崩溃
- recover:捕获异常,恢复程序或做收尾工作

revocer 调用后,抛出的 panic 将会在此处终结,不会再外抛,但是 recover,并不能任意使用,它有强制要求,必须得在 defer 下才能发挥用途。


# 14 语法规则:理解语句块与作用域
由于 Go 使用的是词法作用域,而词法作用域依赖于语句块。所以在讲作用域时,需要先了解一下 Go 中的语句块是怎么一回事?
## 14.1 显示语句块与隐式语句块
通俗地说,语句块是由花括弧({})所包含的一系列语句。
语句块内部声明的名字是无法被外部块访问的。这个块决定了内部声明的名字的作用域范围,也就是作用域。
用花括弧包含的语句块,属于显示语句块。
在 Go 中还有很多的隐式语句块:

- 主语句块:包括所有源码,对应内置作用域
- 包语句块:包括该包中所有的源码(一个包可能会包括一个目录下的多个文件),对应包级作用域
- 文件语句块:包括该文件中的所有源码,对应文件级作用域
- for 、if、switch等语句本身也在它自身的隐式语句块中,对应局部作用域

前面三点好理解,第四点举几个例子
for 循环完后,不能再使用变量 i

for i := 0; i < 5; i++ {
fmt.Println(i)
}

if 语句判断完后,同样不能再使用变量 i

if i := 0; i >= 0 {
fmt.Println(i)
}

switch 语句完了后,也是不是再使用变量 i

switch i := 2; i * 4 {
case 8:
fmt.Println(i)
default:
fmt.Println(“default”)
}

且每个 switch 语句的子句都是一个隐式的语句块

switch i := 2; i * 4 {
case 8:
j := 0
fmt.Println(i, j)
default:
// "j" is undefined here
fmt.Println(“default”)
}
// "j" is undefined here

## 14.2 四种作用域的理解
变量的声明,除了声明其类型,其声明的位置也有讲究,不同的位置决定了其拥有不同的作用范围,说白了就是我这个变量,在哪里可用,在哪里不可用。
根据声明位置的不同,作用域可以分为以下四个类型:

- 内置作用域:不需要自己声明,所有的关键字和内置类型、函数都拥有全局作用域
- 包级作用域:必須函数外声明,在该包内的所有文件都可以访问
- 文件级作用域:不需要声明,导入即可。一个文件中通过import导入的包名,只在该文件内可用
- 局部作用域:在自己的语句块内声明,包括函数,for、if 等语句块,或自定义的 {} 语句块形成的作用域,只在自己的局部作用域内可用

以上的四种作用域,从上往下,范围从大到小,为了表述方便,我这里自己将范围大的作用域称为高层作用域,而范围小的称为低层作用域。
对于作用域,有以下几点总结:

- 低层作用域,可以访问高层作用域
- 同一层级的作用域,是相互隔离的
- 低层作用域里声明的变量,会覆盖高层作用域里声明的变量

在这里要注意一下,不要将作用域和生命周期混为一谈。声明语句的作用域对应的是一个源代码的文本区域;它是一个编译时的属性。
而一个变量的生命周期是指程序运行时变量存在的有效时间段,在此时间区域内它可以被程序的其他部分引用;是一个运行时的概念。
## 14.3 静态作用域与动态作用域
根据局部作用域内变量的可见性,是否是静态不变,可以将编程语言分为如下两种:

- 静态作用域,如 Go 语言
- 动态作用域,如 Shell 语言

具体什么是动态作用域,这里用 Shell 的代码演示一下,你就知道了

!/bin/bash

func01() {
local value=1
func02
}
func02() {
echo "func02 sees value as ${value}"
}

执行函数

func01
func02

从代码中,可以看到在 func01 函数中定义了个局部变量 value,按理说,这个 value 变量只在该函数内可用,但由于在 shell 中的作用域是动态的,所以在 func01中也可以调用 func02 时,func02 可以访问到 value 变量,此时的 func02 作用域可以当成是 局部作用域中(func01)的局部作用域。
但若脱离了 func01的执行环境,将其放在全局环境下或者其他函数中, func02 是访问不了 value 变量的。
所以此时的输出结果是

func02 sees value as 1
func02 sees value as

但在 Go 中并不存在这种动态作用域,比如这段代码,在func01函数中,要想取得 name 这个变量,只能从func01的作用域或者更高层作用域里查找(文件级作用域,包级作用域和内置作用域),而不能从调用它的另一个局部作用域中(因为他们在层级上属于同一级)查找。

import "fmt"

func func01() {
fmt.Println("在 func01 函数中,name:", name)
}

func main() {
var name string = "Python"
fmt.Println("在 main 函数中,name:", name)

func01()

}

因此你在执行这段代码时,会报错,提示在func01中的name还未定义。

标签:case,基础知识,int,fmt,第一章,func,Println,main
From: https://www.cnblogs.com/liuyangQAQ/p/18074244

相关文章

  • 【Java面试题-基础知识02】Java抽象类和接口六连问?
    1、抽象类和接口分别是什么?抽象类是一种类,可以包含抽象方法和非抽象方法,抽象方法是没有具体实现的方法,需要在子类中被具体实现。接口是一种完全抽象的类,其中的所有方法都是抽象方法,没有方法体,它只是定义了一组方法的契约。2、接口中一定不可以有实现方法吗?不一定,Java8引入......
  • 【基础知识整理】时间复杂度 & 空间复杂度
    原文链接:https://blog.csdn.net/fumeidonga/article/details/131070661时间复杂度是指执行算法所需时间的增长率,而空间复杂度则是指执行算法所需存储空间的增长率。一、时间复杂度通常与输入数据进行比较,时间复杂度不是指具体的时间,而是算法的运算次数,是相对于问题规模的相对量......
  • 第一章-微服务简介
    第一章微服务简介微服务是一种软件架构风格,以专注于单一职责的很多小型项目为基础,组合出复杂的大型应用基于单体项目:黑马商城演示单体架构到微服务架构的演变过程,分析其中存在的问题,以及微服务技术如何解决这些问题。每一个微服务技术都是在解决服务化过程中产生的问题。软......
  • docker基础知识
    Docker容器基础介绍和操作-清白之年980410-博客园<linkrel="stylesheet"href="/css/blog-common.min.css?v=g-c5Yfdgh3oAoyQibjhmJ6ylVcBcMRHNIG6JkF70hpY"/><linkid="MainCss"rel="stylesheet"href="/skins/mountainink......
  • 图解Java并发编程第一章总结【精炼版】
    【第一章】图解Java并发编程Java线程的基本操作yield操作:yield操作,在基于时间片轮转的cpu调度算法中,用来放弃当前时间片sleep操作:sleep操作分为三种情况普通sleep:在指定时间内放弃cpu使用权,不释放同步锁sleep(0):作用与yield相同sleep被中断:抛出中断异常......
  • 软件工程 第一章 软件与软件工程
    软件工程第一章软件和软件工程软件的定义computer:有硬件与软件组成硬件:看得见摸得着的电子机械设备块软件:依附在硬件上面的程序,数据和文件的结合,是指挥计算机工作的神经中枢.第一阶段:软件:=程序(弹道计算)input:纸带output:纸带第二阶段:软件:=程序+数据第三......
  • 【Java面试题-基础知识01】Java数据类型四连问?
    一、Java中的基础数据类型有哪些?Java中的基本数据类型包括:1.byte:8位有符号整数,范围为-128到127。2.short:16位有符号整数,范围为-32768到32767。3.int:32位有符号整数,范围为-2147483648到2147483647。4.long:64位有符号整数,范围为-9223372036854775808到9223372036854775807。5.......
  • go语言入门基础知识
    目录序安装常用命令一、数据类型1.布尔值2.字符串字符串遍历3.字符4.整型位运算5.浮点6.复数7.map二、常量、变量1.变量2.常量3.预定义常量iota4.枚举三、流程控制1.条件语句2.选择语句3.循环语句4.跳转语句四、函数不定参数匿名函数与闭包make()函数new函数闭包defer五、数组1.......
  • Android Framework基础知识
    哈喽大家好,我是Zzz.给大家分享一篇Framework入门的基础知识文章,内容纯纯原创。一、Application,Activity和进程的关系?  Application、Activity只是进程虚拟机运行的一个类对象,只是属于系统的一个组件和进程没有直接联系。Android支持为每个组件可以单独进程方式运行。 ......
  • Django基础知识点一
    Django基础知识点【零】补充方法【1】Django项目测试if__name__=='__main__':importosimportdjangoos.environ.setdefault('DJANGO_SETTINGS_MODULE','BookSystem.settings')django.setup()'''测试代码''......