目录
常量
Go语言中的常量使用关键字const
定义,用于存储不会改变的数据,常量是在编译时被创建的,即使定义在函数内部也是如此,并且只能是布尔型
、数字型
(整数型、浮点型和复数)和字符串型
。
由于编译时的限制,定义常量的表达式必须为能被编译器求值的常量表达式。
声明格式:
const name [type] = value
const a1 int8 = 9
type可以省略,和变量声明一样,可以批量声明多个常量。
所有常量的运算都可以在编译期完成,常量间的所有算术运算、逻辑运算、比较运算的结果、对常量的类型转换操作或以下函数调用都是返回常量。len、cap、real、imag、complex 和 unsafe.Sizeof。
如果是批量声明的常量,除了第一个外其它的常量右边的初始化表达式都可以省略,如果省略初始化表达式则表示使用前面常量的初始化表达式,对应的常量类型也是一样的。例如:
const a1 int8 = 9
const (
a = 1
b
c = 13.0
d
)
func main() {
fmt.Println(a)
fmt.Printf("%d %d %.2f %.2f\n", a, b, c, d)
}
iota常量生成器
常量声明可以使用 iota 常量生成器初始化,它用于生成一组以相似规则初始化的常量,但是不用每行都写一遍初始化表达式。
在一个 const 声明语句中,在第一个声明的常量所在的行,iota 将会被置为 0,然后在每一个有常量声明的行加1
const (
Sunday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)
func main() {
fmt.Println(Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday)
}
指针
指针在Go语言中分为两种:
- 类型指针,允许对这个指针类型的数据进行修改,传递数据可以直接使用指针,而无须拷贝数据,类型指针不能进行偏移和运算。
- 切片,由指向起始元素的原始指针、元素数量和容量组成。
指针类型变量拥有指针高效访问的特点,又不会发生指针偏移,从而避免了非法修改关键性数据的问题。垃圾回收
也比较容易对不会发生偏移的指针进行检索和回收。
切片比原始指针具备更强大的特性,而且更为安全。切片在发生越界时,运行时会报出宕机,并打出堆栈,而原始指针只会崩溃。
理解指针
在内存中开辟了一片空间,空间内存放着数值,这片空间在整个内存当中,有一个唯一的地址,用来进行标识,指向这个地址的变量就称为指针。
一个指针变量可以指向任何一个值的内存地址,它所指向的值的内存地址在 32 和 64 位机器上分别占用 4 或 8 个字节,占用字节的大小与所指向的值的大小无关。
当一个指针被定义后没有分配到任何变量
时,它的默认值为 nil
。每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。
Go语言中使用在变量名前面添加&
操作符(前缀)来获取变量的内存地址(取地址操作),格式如下:
//其中 v 代表被取地址的变量,变量 v 的地址使用变量 ptr 进行接收,ptr 的类型为*T,称做 T 的指针类型,*代表指针。
ptr := &v // v 的类型为 T
func main() {
a := 1
str := "贝塔"
var aptr *int = &a
var sptr *string = &str
fmt.Printf("%p %p \n", aptr, sptr)
}
变量、指针和地址三者的关系是,每个变量都拥有地址,指针的值就是地址
当使用
&
操作符对普通变量进行取地址操作并得到变量的指针后,可以对指针使用*
操作符,也就是指针取值
取地址操作符&
和取值操作符*
是一对互补操作符,&
取出地址,*
根据地址取出地址指向的值
变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:
- 对变量进行取地址操作使用
&
操作符,可以获得这个变量的指针变量。 - 指针变量的值是指针地址。
- 对指针变量进行取值操作使用
*
操作符,可以获得指针变量指向的原变量的值。
指针修改值
可以通过指针修改指针指向的值。
func main() {
var a = 10
// 10
fmt.Printf("main方法中的值=%d\n", a)
update1(a)
// 10
fmt.Printf("update1执行后,main方法中的值=%d\n", a)
update2(&a)
// 99
fmt.Printf("update2执行后,main方法中的值=%d\n", a)
}
func update1(a int) {
a = 66
// 66
fmt.Printf("update1方法内=%d\n", a)
}
func update2(a *int) {
*a = 99
// 99
fmt.Printf("update2方法内的值=%d\n", *a)
}
创建指针
通过new关键字创建,格式:new(类型)
func main() {
ptr := new(string)
*ptr = "贝塔贝塔"
fmt.Println(*ptr)
}
练习
获取命令行的输入信息
var mode = flag.String("name", "", "请输入你的名字")
func main() {
flag.Parse()
fmt.Printf("你的名字是:%s", *mode)
}
> go run .\main2.go --name 贝塔贝塔
变量的生命周期
变量的生命周期指的是在程序运行期间变量有效存在的时间间隔。
变量的生命周期分为:
- 全局变量:它的生命周期和整个程序的运行周期是一致的;
- 局部变量:它的生命周期则是动态的,从创建这个变量的声明语句开始,到这个变量不再被引用为止;
- 形式参数和函数返回值:都属于局部变量,在函数被调用的时候创建,函数调用结束后被销毁。
go的内存中应用了两种数据结构用于存放变量:
- 堆(heap):堆是用于存放进程执行中被动态分配的内存段。它的大小并不固定,可动态扩张或缩减。当进程调用 malloc 等函数分配内存时,新分配的内存就被动态加入到堆上(堆被扩张)。当利用 free 等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减);
- 栈(stack):栈又称堆栈, 用来存放程序暂时创建的局部变量,也就是我们函数的大括号
{ }
中定义的局部变量。
栈是先进后出,往栈中放元素的过程,称为入栈,取元素的过程称为出栈。
栈可用于内存分配,栈的分配和回收速度非常快
在程序的编译阶段,编译器会根据实际情况自动选择
在栈
或者堆
上分配局部变量的存储空间,不论使用 var 还是 new 关键字声明变量都不会影响编译器的选择。
var global *int
// 变量 x 必须在堆上分配,因为它在函数退出后依然可以通过包一级的 global 变量找到,虽然它是在函数内部定义的。这个局部变量 x 从函数 f 中逃逸了。
func f() {
var x int
x = 1
global = &x
}
// 当函数 g 返回时,变量 y 不再被使用,也就是说可以马上被回收的。因此,y 并没有从函数 g 中逃逸,编译器可以选择在栈上分配 *y 的存储空间,也可以选择在堆上分配,然后由Go语言的 GC(垃圾回收机制)回收这个变量的内存空间。
func g() {
y := new(int)
*y = 1
}
类型别名
// 定义IntNew为int类型 ,定义之后 IntNew为一种新的类型
type IntNew int
// TypeAlias 只是 Type 的别名,本质上 TypeAlias 与 Type 是同一个类型
type IntAlias = int
func main() {
var a IntNew
var b IntAlias
// aType=main.IntNew
fmt.Printf("aType=%T\n", a)
// bType=int
fmt.Printf("bType=%T\n", b)
}
注释
注释主要分成两类,分别是单行注释和多行注释。
- 单行注释简称行注释,是最常见的注释形式,可以在任何地方使用以
//
开头的单行注释; - 多行注释简称块注释,以
/*
开头,并以*/
结尾,且不可以嵌套使用,多行注释一般用于包的文档描述或注释成块的代码片段。
每一个包都应该有相关注释,在使用 package 语句声明包名之前添加相应的注释,用来对包的功能及作用进行简要说明。在 package 语句之前的注释内容将被默认认为是这个包的文档说明。一个包可以分散在多个文件中,但是只需要对其中一个进行注释说明即可。
关键字和标识符
关键字一共有 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 |
标识符指对各种变量、方法、函数等命名时使用的字符序列,标识符由若干个字母、下划线_
、和数字组成,且第一个字符必须是字母。
下划线_
是一个特殊的标识符,称为空白标识符
标识符的命名需要遵守以下规则:
- 由 26 个英文字母、0~9、
_
组成; - 不能以数字开头,例如
var 1num int
是错误的; - Go语言中严格区分大小写;
- 标识符不能包含空格;
- 不能以系统保留关键字作为标识符,比如 break,if 等等。
命名标识符时还需要注意以下几点:
- 标识符的命名要尽量采取简短且有意义;
- 不能和标准库中的包名重复;
- 为变量、函数、常量命名时采用驼峰命名法,例如 stuName、getVal;
预定义标识符一共有 36 个,主要包含基础数据类型和内置函数,这些预定义标识符也不可以当做标识符来使用。
append | bool | byte | cap | close | complex | complex64 | complex128 | uint16 |
---|---|---|---|---|---|---|---|---|
copy | false | float32 | float64 | imag | int | int8 | int16 | uint32 |
int32 | int64 | iota | len | make | new | nil | panic | uint64 |
println | real | recover | string | true | uint | uint8 | uintptr |
运算符优先级
优先级值越大,表示优先级越高。
优先级 | 分类 | 运算符 | 结合性 |
---|---|---|---|
1 | 逗号运算符 | , | 从左到右 |
2 | 赋值运算符 | =、+=、-=、*=、/=、 %=、 >=、 <<=、&=、^=、|= | 从右到左 |
3 | 逻辑或 | || | 从左到右 |
4 | 逻辑与 | && | 从左到右 |
5 | 按位或 | | | 从左到右 |
6 | 按位异或 | ^ | 从左到右 |
7 | 按位与 | & | 从左到右 |
8 | 相等/不等 | ==、!= | 从左到右 |
9 | 关系运算符 | <、<=、>、>= | 从左到右 |
10 | 位移运算符 | <<、>> | 从左到右 |
11 | 加法/减法 | +、- | 从左到右 |
12 | 乘法/除法/取余 | *(乘号)、/、% | 从左到右 |
13 | 单目运算符 | !、*(指针)、& 、++、–、+(正号)、-(负号) | 从右到左 |
14 | 后缀运算符 | ( )、[ ]、-> | 从左到右 |
字符串转换
整数与字符串
func main() {
str := "666"
iVal, _ := strconv.Atoi(str)
fmt.Printf("%d %T \n", iVal, iVal)
sVal := strconv.Itoa(iVal)
fmt.Printf("%s %T \n", sVal, sVal)
}
浮点型与字符串
func main() {
str := "3.1415"
fValue, _ := strconv.ParseFloat(str, 64)
fmt.Printf("%f %T \n", fValue, fValue)
//4个参数,1:要转换的浮点数 2. 格式标记(b、e、E、f、g、G)
//3. 精度 4. 指定浮点类型(32:float32、64:float64)
// 格式标记:
// ‘b’ (-ddddp±ddd,二进制指数)
// ‘e’ (-d.dddde±dd,十进制指数)
// ‘E’ (-d.ddddE±dd,十进制指数)
// ‘f’ (-ddd.dddd,没有指数)
// ‘g’ (‘e’:大指数,‘f’:其它情况)
// ‘G’ (‘E’:大指数,‘f’:其它情况)
//
// 如果格式标记为 ‘e’,‘E’和’f’,则 prec 表示小数点后的数字位数
// 如果格式标记为 ‘g’,‘G’,则 prec 表示总的数字位数(整数部分+小数部分)
sValue := strconv.FormatFloat(fValue, 'f', -1, 64)
fmt.Printf("%s %T \n", sValue, sValue)
}
练习
var level = 1
var exp = 0
func main() {
fmt.Println("请输入你的角色名")
reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\n')
if err != nil {
panic(err)
}
name := input[:len(input)-1]
fmt.Printf("%s,您好,欢迎来到打怪兽,目前等级%d\n", name, level)
s := `你遇到一个史莱姆,选择战斗还是逃跑
1:战斗
2:逃跑
exit:退出游戏`
fmt.Println(s)
for {
input, err := reader.ReadString('\n')
if err != nil {
panic(err)
}
selector := input[:len(input)-1]
switch selector {
case "1":
exp += 10
fmt.Println("您杀死了史莱姆,获得了10经验!")
if exp <= 10 {
level = 2
} else if exp <= 20 {
level = 3
} else if exp <= 30 {
level = 4
} else {
level = 999
}
fmt.Printf("您目前的等级为:%d\n", level)
continue
case "2":
fmt.Println("逃跑成功!------------")
fmt.Println(s)
continue
case "exit":
fmt.Println("退出游戏!")
os.Exit(0)
default:
fmt.Println("非法指令,请重新输入!")
}
}
}
数组
数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。
因为数组的长度是固定的,所以在Go语言中很少直接使用数组。
- 数组变量名:数组声明及使用时的变量名。
- 元素数量:数组的元素数量,可以是一个表达式,但最终通过编译期计算的结果必须是整型数值,元素数量不能含有到运行时才能确认大小的数值。
- Type:可以是任意基本类型,包括数组本身,类型为数组本身时,可以实现多维数组。
var 数组变量名 [元素数量]Type
//默认数组中的值是类型的默认值
var arr [3]int
func main() {
// 通过下标获取值
fmt.Printf("%d %d %d \n", arr[0], arr[1], arr[2])
// 通过for range 获取索引及值
for idx, val := range arr {
fmt.Printf("idx = %d value = %d |", idx, val)
}
fmt.Println()
// 通过for range 获取值
for _, val := range arr {
fmt.Printf("value=%d |", val)
}
// 通过for range 获取下标索引
fmt.Println()
for idx := range arr {
fmt.Printf("index=%d |", idx)
}
}
赋值
var arr [3]int = [3]int{1, 2, 3}
// 如果第三个不赋值,就是默认值0
var arr1 [3]int = [3]int{1, 2}
// 如果不写数据数量,而使用...,表示数组的长度是根据初始化值的个数来计算
var arr2 [3]int = [...]int{1, 2, 3}
func main() {
arr := [3]int{1, 2, 3}
fmt.Printf("%d", arr)
// 通过下标进行赋值
arr[0] = 4
arr[1] = 5
arr[2] = 6
}
定义新类型
type arr3 [3]int
var arr arr3
func main() {
arr[0] = 1
arr[1] = 2
// [1 2 0] main.arr3
fmt.Printf("%d %T", arr, arr)
}
通过索引赋值
var arr [3]int
func main() {
arr = [3]int{1: 5}
// [0 5 0]
fmt.Printf("%d", arr)
}
数组比较
如果两个数组类型相同(包括数组的长度,数组中元素的类型)的情况下,我们可以直接通过较运算符(==
和!=
)来判断两个数组是否相等,只有当两个数组的所有元素都是相等的时候数组才是相等的,不能比较两个类型不同的数组,否则程序将无法完成编译。
func main() {
a := [2]int{1, 2}
b := [...]int{1, 2}
c := [2]int{1, 3}
// true true true
fmt.Printf("%t %t %t \n", a == b, b != c, a != c)
d := [3]int{1, 2}
// 编译错误
fmt.Printf("%t", a == d)
}
多维数组
Go语言中允许使用多维数组,因为数组属于值类型,所以多维数组的所有维度都会在创建时自动初始化零值,多维数组尤其适合管理具有父子关系或者与坐标系相关联的数据。
// 声明一个二维整型数组,两个维度的长度分别是 4 和 2
var arr [4][2]int
// 使用数组字面量来声明并初始化一个二维整型数组
var arr1 = [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}
// 声明并初始化数组中索引为 1 和 3 的元素
var arr2 = [4][2]int{1: {20, 21}, 3: {40, 41}}
// 声明并初始化数组中指定的元素
var arr3 = [4][2]int{1: {0: 20}, 3: {1: 41}}
func main() {
// 通过索引下标取值
fmt.Printf("%d\n", arr2[1])
// 循环取值
for idx, val := range arr3 {
fmt.Printf("索引:%d 值:%d \n", idx, val)
}
}
只要类型一致,就可以将多维数组互相赋值。因为数组中每个元素都是一个值,所以可以独立复制某个维度。
func main() {
var arr [2][2]int
arr[0][0] = 10
arr[0][1] = 20
arr[1][0] = 30
arr[1][1] = 40
var arr1 [2][2]int
arr1 = arr
// [[10 20] [30 40]]
fmt.Println(arr1)
// [10 20]
var arr3 [2]int = arr1[0]
fmt.Println(arr3)
// 30
var val int = arr1[1][0]
fmt.Println(val)
}
切片
切片(Slice)
与数组一样,也是可以容纳若干类型相同的元素的容器。
与数组不同的是,无法通过切片类型来确定其值的长度。
每个切片值都会将数组作为其底层数据结构。
我们也把这样的数组称为切片的底层数组
。
切片(slice)
是对数组的一个连续片段的引用,所以切片是一个引用类型。
这个片段可以是整个数组
,也可以是由起始和终止索引标识的一些项的子集
,需要注意的是,终止索引标识的项
不包括在切片内(左闭右开的区间)。
Go语言中切片的内部结构包含地址
、大小
和容量
,切片一般用于快速地操作一块数据集合。
slice [开始位置 : 结束位置]
- slice:表示目标切片对象;
- 开始位置:对应目标切片对象的索引;
- 结束位置:对应目标切片的结束索引。
func main() {
var val = [3]int{1, 2, 3}
slice := val[1:2]
fmt.Printf("%d %d %T", val, slice, slice)
}
从数组或切片生成新的切片拥有如下特性:
- 取出的元素数量为:结束位置 - 开始位置;
- 取出元素不包含结束位置对应的索引,切片最后一个元素使用 slice[len(slice)] 获取;
- 当缺省开始位置时,表示从连续区域开头到结束位置
(a[:2])
; - 当缺省结束位置时,表示从开始位置到整个连续区域末尾
(a[0:])
; - 两者同时缺省时,与切片本身等效
(a[:])
; - 两者同时为 0 时,等效于空切片,一般用于切片复位
(a[0:0])
。
func main() {
var val [30]int
for i := 0; i < 30; i++ {
val[i] = i + 1
}
// [11 12 13 14 15]
fmt.Printf("%d\n", val[10:15])
// [21 22 23 24 25 26 27 28 29 30]
fmt.Printf("%d\n", val[20:])
// [1 2]
fmt.Printf("%d\n", val[:2])
}
直接声明
除了可以从原有的数组或者切片中生成切片外,也可以声明一个新的切片
,每一种类型都可以拥有其切片类型
,表示多个相同类型元素的连续集合。
//name 表示切片的变量名,Type 表示切片对应的元素类型。
var name []Type
func main() {
// 声明字符串切片
var strList []string
// 声明整型切片
var val1 []int
// 声明一个空切片
var val2 []int = []int{}
// [] [] []
fmt.Println(strList, val1, val2)
// 0 0 0
fmt.Println(len(strList), len(val1), len(val2))
// true true false
fmt.Println(strList == nil, val1 == nil, val2 == nil)
// 可以使用 append() 函数向切片中添加元素。
strList = append(strList, "贝塔贝塔")
fmt.Println(strList)
}
make
如果需要动态地创建一个切片,可以使用 make() 内建函数。
make( []Type, size, cap )
Type
是指切片的元素类型,size
指的是为这个类型分配多少个元素,cap
为预分配的元素数量,这个值设定后不影响 size,只是能提前分配空间,降低多次分配空间造成的性能问题
。
func main() {
var a = make([]int, 2)
b := make([]int, 2, 3)
// [0 0] [0 0]
fmt.Println(a, b)
// 2 2
fmt.Println(len(a), len(b))
// 2 3
fmt.Println(cap(a), cap(b))
}
使用 make() 函数生成的切片一定发生了内存分配操作,但给定开始与结束位置(包括切片复位)的切片只是将新的切片结构指向已经分配好的内存区域,设定开始与结束位置,不会发生内存分配操作。
func main() {
number := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9}
mySlice := number[4:6]
fmt.Printf("长度:%d 值:%d\n", len(mySlice), mySlice)
fmt.Println(cap(mySlice))
mySlice = mySlice[:cap(mySlice)]
fmt.Printf("第四个元素为:%d", mySlice[3])
}
切片复制
内置函数 copy() 可以将一个数组切片复制到另一个数组切片中,如果加入的两个数组切片不一样大,就会按照其中较小的那个数组切片的元素个数进行复制。
copy( destSlice, srcSlice []T) int
srcSlice
为数据来源切片,destSlice
为复制的目标(也就是将 srcSlice 复制到 destSlice),目标切片必须分配过空间且足够承载复制的元素个数
,并且来源和目标的类型必须一致
,copy() 函数的返回值表示实际发生复制的元素个数。
func main() {
slice1 := []int{1, 2, 3, 4, 5}
slice2 := []int{7, 6, 5}
只会复制slice2的前3个元素到slice1中
copy(slice1, slice2)
// [7 6 5 4 5]
fmt.Println(slice1)
slice2 = []int{1, 1, 1}
只会复制slice1的前3个元素到slice2中
copy(slice2, slice1)
// [7 6 5]
fmt.Println(slice2)
}
切片的引用和复制操作对切片元素的影响:
func main() {
s1 := make([]int, 1000)
for i := 0; i < 1000; i++ {
s1[i] = i
}
// 引用切片数据 切片不会因为等号操作进行元素的复制
s2 := s1
s3 := make([]int, 1000)
copy(s3, s1)
s1[0] = 123
// s2:123 s3:0
fmt.Printf("s2:%d s3:%d\n", s2[0], s3[0])
copy(s3, s1[4:6])
// 4 5 2 3 4
for i := 0; i < 5; i++ {
fmt.Printf("%d ", s3[i])
}
}
map
map 是一种无序的键值对
的集合。可以像迭代数组和切片那样迭代它。map 是引用类型,使用如下方式声明:
//[keytype] 和 valuetype 之间允许有空格。
- mapname 为 map 的变量名。
- keytype 为键类型。
- valuetype 是键对应的值类型。
var mapname map[keytype]valuetype
在声明的时候不需要知道 map 的长度,因为 map 是可以动态增长的,未初始化的 map 的值是 nil,使用函数 len() 可以获取 map 中 键值对的数目。
func main() {
var map1 = map[string]int{"k1": 1, "k2": 2}
var map2 map[string]int
// map2 是 map1 的引用,对 map2 的修改也会影响到 map1 的值。
map2 = map1
map2["k1"] = 99
// 99 99 0 key不存在则返回value的初始值
fmt.Println(map1["k1"], map2["k1"], map1["k99"])
}
map的另外一种创建方式:
当 map 增长到容量上限的时候,如果再增加新的 key-value,map 的大小会自动加 1,所以出于性能的考虑,对于大的 map 或者会快速扩张的 map,即使只是大概知道容量,也最好先标明。
map 可以根据新增的 key-value 动态的伸缩,因此它不存在固定长度或者最大限制,但是也可以选择标明 map 的初始容量 capacity。
make(map[keytype]valuetype, cap)
一个 key 只能对应一个 value, value 又是一个原始类型,使用切片可以实现一个 key对应多个值。
mp1 := make(map[int][]int)
mp2 := make(map[int]*[]int)
遍历
map 的遍历使用 for range 循环完成。
func main() {
var map1 map[string]string
map1 = make(map[string]string, 3)
map1["k1"] = "key1"
map1["k2"] = "key2"
map1["k3"] = "key3"
for key, value := range map1 {
fmt.Printf("key:%s value:%s \n", key, value)
}
}
删除
使用 delete() 内建函数从 map 中删除一组键值对。
Go语言中并没有为 map 提供任何清空所有元素的函数、方法,清空 map 的唯一办法就是重新 make 一个新的 map,并行垃圾回收效率比写一个清空函数要高效的多。
func main() {
map1 := make(map[string]int, 3)
map1["a"] = 1
map1["b"] = 2
map1["c"] = 3
delete(map1, "b")
fmt.Println(map1)
//map1 = map[string]int{}
map1 = make(map[string]int)
fmt.Println(map1)
}
并发问题
map 在并发情况下,只读是线程安全的,同时读写是线程不安全的。
运行代码会报错,输出如下:
fatal error: concurrent map read and map write
两个并发函数不断地对 map 进行读和写而发生了竞态问题,map 内部会对这种并发操作进行检查并提前发现。
func main() {
map1 := make(map[string]int)
go func() {
for {
map1["a"] = 1
}
}()
go func() {
for {
_ = map1["a"]
}
}()
for {
}
}
sync.Map
sync.Map 和 map 不同,不是以语言原生形态提供,而是在 sync 包下的特殊结构。
sync.Map 有以下特性:
- 无须初始化,直接声明即可。
- sync.Map 不能使用 map 的方式进行取值和设置等操作,而是使用 sync.Map 的方法进行调用,Store 表示存储,Load 表示获取,Delete 表示删除。
- 使用 Range 配合一个回调函数进行遍历操作,通过回调函数返回内部遍历出来的值,Range 参数中回调函数的返回值在需要继续迭代遍历时,返回 true,终止迭代遍历时,返回 false。
func main() {
var syncMap sync.Map
// 将键值对保存到sync.Map
syncMap.Store("a", 1)
syncMap.Store("b", 2)
syncMap.Store("c", 3)
// 从sync.Map中根据键取值
fmt.Println(syncMap.Load("a"))
// 根据键删除对应的键值对
syncMap.Delete("b")
syncMap.Range(func(k, v interface{}) bool {
fmt.Printf("%s %d \n", k, v)
return true
})
}
nil
布尔类型的零值(初始值)为 false,数值类型的零值为 0,字符串类型的零值为空字符串""
,而指针、切片、映射、通道、函数和接口的零值则是 nil。
nil 标识符是不能比较的,nil 没有默认类型,不同类型 nil 的指针是一样的
func main() {
var a []int
var b *int
// 0x0 0x0 不同类型 nil 的指针是一样的
fmt.Printf("%p %p \n", a, b)
var m map[string]string
var sl []int
var ptr *int
var c chan int
var f func()
var i interface{}
// nil 是 map、slice、pointer、channel、func、interface 的零值
fmt.Printf(" %#v\n %#v\n %#v\n %#v\n %#v\n %#v\n", m, sl, ptr, c, f, i)
// 不同类型的 nil 值占用的内存大小可能是不一样的,具体的大小取决于编译器和架构
var p *struct{}
// 8
fmt.Printf("%d\n", unsafe.Sizeof(p))
var m1 map[int]string
// 8
fmt.Println(unsafe.Sizeof(m1))
var s []int
// 24
fmt.Println(unsafe.Sizeof(s))
var c1 chan string
// 8
fmt.Println(unsafe.Sizeof(c1))
var f1 func()
// 8
fmt.Println(unsafe.Sizeof(f1))
var i1 interface{}
// 16
fmt.Println(unsafe.Sizeof(i1))
}
new和make
make 关键字的主要作用是创建 slice、map 和 Channel 等内置的数据结构,而 new 的主要作用是为类型申请一片内存空间,并返回指向这片内存的指针。
- make 分配空间后,会进行初始化,new分配的空间被清零
- new 分配返回的是指针,即类型 *Type。make 返回引用,即 Type;
- new 可以分配任意类型的数据;
流程控制
if else
关键字if
是用于测试某个条件(布尔型或逻辑型)的语句,如果该条件成立,则会执行 if 后由大括号{}
括起来的代码块,否则就忽略该代码块继续执行后续的代码。
if condition1 {
// condition1 满足 执行
} else if condition2 {
// condition1 不满足 condition2满足 执行
}else {
// condition1和condition2都不满足 执行
}
if 还有一种特殊的写法,可以在 if 表达式之前添加一个执行语句,再根据变量值进行判断,代码如下:
// 这种写法可以将返回值与判断放在一行进行处理,而且返回值的作用范围被限制在 if、else 语句组合中。
if a := 5; a > 0 {
fmt.Println(a)
}
for
GO语言中的循环语句只支持 for 关键字。
func main() {
//i := 0; 赋初值,i<10 循环条件 如果为真就继续执行 ;i++ 后置执行 执行后继续循环
for i := 0; i < 5; i++ {
fmt.Println(i)
}
var a int
// 相当于while(true),没有break就无限循环
for {
a++
if a == 2 {
break
}
}
i := 0
//初值可以省略,但是;必须有,但是这样写i的作用域就比较大了,脱离了for循环
for ; i < 3; i++ {
fmt.Println(i)
}
b := 0
// 将 if 判断整合到 for 中
for b < 2 {
fmt.Println(b)
b++
}
}
**跳出循环的几种方式:**break、return、panic、goto
for i := 0; i < 5; i++ {
fmt.Println(i)
if i == 2 {
break
}
}
fmt.Println("end")
for i := 0; i < 5; i++ {
fmt.Println(i)
if i == 2 {
return
}
}
fmt.Println("end")
for i := 0; i < 5; i++ {
fmt.Println(i)
if i == 2 {
panic("exception")
}
}
fmt.Println("end")
func main() {
for i := 0; i < 10; i++ {
for j := 0; j < 10; j++ {
if j == 2 {
goto breakHere
}
}
}
// 手动返回, 避免执行进入标签
return
breakHere:
fmt.Println("end")
}
代码优化
func main() {
s := "123123123"
for i, n := 0, length(s); i < n; i++ {
println(i, s[i])
}
}
func length(str string) int {
fmt.Println("execute length")
return len(str)
}
// 九九乘法表
func main() {
for y := 1; y <= 9; y++ {
for x := 1; x <= y; x++ {
fmt.Printf("%d*%d=%d\t", x, y, x*y)
}
fmt.Println()
}
}
for range
for range 可以遍历数组、切片、字符串、map 及管道(channel)。
val
始终为集合中对应索引的值拷贝
,因此它一般只具有只读性质,对它所做的任何修改都不会影响到集合中原有的值
通过 for range 遍历的返回值有一定的规律:
- 数组、切片、字符串返回索引和值。
- map 返回键和值。
- channel只返回管道内的值。
map1 := make(map[string]string, 3)
map1["a"] = "1"
map1["b"] = "2"
map1["c"] = "3"
for k, v := range map1 {
fmt.Printf("key:%s value:%s \n", k, v)
if k == "a" {
v = "AAA"
fmt.Printf("updated: %s\n", map1[k])
}
}
//因为一个字符串是 Unicode 编码的字符(或称之为 rune )集合
//char 实际类型是 rune 类型
str := "你好贝塔"
for idx, s := range str {
fmt.Printf("%d %c \n", idx, s)
}
switch
switch 语句用于基于不同条件执行不同动作,每一个 case 分支都是唯一的,从上至下逐一测试,直到匹配为止。
switch 分支表达式可以是任意类型,不限于常量。可省略 break,默认自动终止。
func main() {
var exp = 100
var level int
// 类型不被局限于常量或整数,但必须是相同的类型;或者最终结果为相同类型的表达式。
// 可以同时测试多个可能符合条件的值,使用逗号分割它们。
switch exp {
case 50, 60, 70:
level = 7
case 80:
level = 8
case 100:
level = 10
default:
level = 5
}
//swtich后面如果没有条件表达式,则会对true进行匹配
switch {
case level == 7:
fmt.Println("平均等级")
case level > 7:
fmt.Println("大佬等级")
case level < 7:
fmt.Println("菜鸟等级")
}
}
switch默认相当于每个case最后带有break,匹配成功后不会自动向下执行其他case,而是跳出整个switch,可以通过fallthrough
关键字执行下一个case。
加了fallthrough后,会直接运行【紧跟的后一个】case或default语句,不论条件是否满足都会执行
// GOGO Lets GO
str := "GOGO"
switch str {
case "GOGO":
fmt.Print("GOGO")
fallthrough
case "Lets GO":
fmt.Print(" Lets GO")
}
goto
goto 语句通过标签进行代码间的无条件跳转。
使用 goto 退出多层循环
func main() {
for x := 0; x < 10; x++ {
for y := 0; y < 10; y++ {
if y == 2 {
goto breakPoint
}
}
}
fmt.Println("done")
breakPoint:
fmt.Println("goto break")
}
使用 goto 集中处理错误
func main() {
err := errors.New("Error1")
if err != nil {
goto errorPoint
}
err = errors.New("Error2")
if err != nil {
goto errorPoint
}
fmt.Println("done")
return
errorPoint:
fmt.Println(err)
os.Exit(1)
}
break
break 语句可以结束 for、switch 和 select 的代码块,另外 break 语句还可以在语句后面添加
标签
,表示退出某个标签对应的代码块,标签
要求必须定义在对应的for
、switch
和select
的代码块上。
func main() {
breakPoint:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if j == 1 {
break breakPoint
}
fmt.Println("j:", j)
}
}
}
continue
continue 语句可以结束当前循环,开始下一次的循环迭代过程,仅限在 for 循环内使用,在 continue 语句后添加
标签
时,表示开始标签对应的循环
func main() {
continuePoint:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if j == 1 {
continue continuePoint
}
fmt.Println("j:", j)
}
}
}