[1] 基本是看菜鸟的Go语言教程,整理了一些可能重要的部分 点击这里跳转
[2] 后面看了一点C语言编程网关于Go的介绍,略有参考 点击这里跳转
[3] 翻阅了Go语言趣学指南》大部分章节,颇为收益
0 常用命令
0.1 printf
fmt.Printf()
方法接受的第一个参数总是文本,后面的参数是表达式。
- 利用
/n
可以实现换行效果 - 利用
%4v
或者%-4v
可以实现指定宽度4,多余空格填充根据正负号至文本左边或者右边。 - 对于浮点数类型,
%.2f
表示保留两位小数。同样%4.2f
表示保留两位小时同时整体宽度为4,宽度比字符大时,左侧填充空格。
//导包:import "fmt"
%d 整形
%f 浮点数
%s 正常输出字符串
%t 布尔类型
%c 打印出字符 类型为rune或byte
%T 打印该变量的类型
%v 默认格式的值,似乎所有的格式都可以(推荐)
0.2 rand
import "math/rand" //必须导包
var num = rand.Intn(10) //返回0-9的围随机数
0.3 类型转换
Go语言不允许混合使用不同类型的变量,必须通过类型转换。类型转换的格式为:类型(变量名)
大范围的类型向小范围类型转换时,需要注意范围或者截断误差,超出范围时会出现回绕行为。
- 整数转字符串推荐:
str := strconv.Itoa(num)
- 字符串转整数推荐:
num, err := strconv.Atoi(str)
// 将整数转为string (java里面只用拼接一个空字符串就可以了但go不行)
numInt := 10
str := strconv.Itoa(numInt) //方式一
str2 := fmt.Sprintf("%v", numInt) //方式二
// 将string转为整数
str3 := "11123"
num, err := strconv.Atoi(str3)
if err != nil {
fmt.Println("转换出错")
}
fmt.Println(num)
// int 转 float
numInt := 32766
numFloat := float64(numInt)
// float转int
numFloat := 32767
numInt := int16(numFloat) //回绕变成-32768
// math.MinInt16 math.MaxInt16可以查看类型的最小和最大常量
// rune或byte转string
var pi rune = 960 //pi的utf-8编码值是960
s := string(pi)
fmt.println(s) //打印出希腊字母pi
1 初识Go语言
1.1 概述
Go 是一个开源的编程语言,它能让构造简单、可靠且高效的软件变得容易。
Go是从2007年末由Robert Griesemer, Rob Pike, Ken Thompson主持开发,后来还加入了Ian Lance Taylor, Russ Cox等人,并最终于2009年11月开源,在2012年早些时候发布了Go 1稳定版本。现在Go的开发已经是完全开放的,并且拥有一个活跃的社区。
Go 语言特色:
- 简洁、快速、安全
- 并行、有趣、开源
- 内存管理、数组安全、编译迅速
Go 语言用途
- Go 语言被设计成一门应用于搭载 Web 服务器,存储集群或类似用途的巨型中央服务器的系统编程语言。
- 对于高性能分布式系统领域而言,Go 语言无疑比大多数其它语言有着更高的开发效率。它提供了海量并行的支持,这对于游戏服务端的开发而言是再好不过了。
- (为高并发而生)
1.2 环境安装和HelloWorld
环境安装:
- 下载地址:https://golang.google.cn/dl/
- 版本:go1.17.7.windows-amd64.msi
- 安装位置:C:\DevSoftware\Go
安装GoLand:Go语言的IDE:
- 下载地址:https://www.jetbrains.com/go/
- 版本:2021.3.3
- 安装位置:C:\DevSoftware\GoLand 2021.3.3
无脑安装下一步就好,会自动帮你配置好环境变量
HelloWorld步骤:
1 桌面建文件helloWorld.go
package main import "fmt" func main() { fmt.Println("Hello the beautiful world!") }
2 进入桌面的cmd命令窗口 执行go run命令
go run helloWorld.go
1.3 全局变量和局部变量
全局变量不能用
:=
语法糖的形式赋值,使用:=
时一定要注意左侧变量没有被定义过其实java里面也是类似的概念,少量细节的语法不同罢了
局部变量和全局变量的作用域
- Go语言中局部变量的作用域是从定义的那一行开始, 直到遇到 } 结束或者遇到return为止。在switch语句中,case和default也引入了新的作用域
- Go语言中的全局变量作用域, 只要定义了, 在定义之前和定义之后都可以使用
局部变量和全局变量的生命周期
- Go语言局部变量的生命周期, 只有执行了才会分配存储空间, 只要离开作用域就会自动释放, Go语言的局部变量存储在栈区
- Go语言全局变量的生命周期, 只要程序一启动就会分配存储空间, 只有程序关闭才会释放存储空间, Go语言的全局变量存储在静态区(数据区)
注意点
- 相同的作用域内, 无论是全局变量还是局部变量, 都不能出现同名的变量
- 局部变量如果没有使用, 编译会报错, 全局变量如果没有使用, 编译不会报错
- 在Go语言中局部变量没有初始化, 会默认初始化为0
- :=只能用于局部变量, 不能用于全局变量
1.4 package包
Go语言是使用包来组织源代码的,包(package)是多个 Go 源码的集合,是一种高级的代码复用方案。Go语言中为我们提供了很多内置包,如 fmt、os、io 等。
package main //声明自己所在的包
在执行 main 包的 mian 函数之前,Go引导程序会先对整个程序的包进行初始化。整个执行的流程如下图所示。
标准的Go语言代码库中包含了大量的包,并且在安装 Go 的时候多数会自动安装到系统中。我们可以在 $GOROOT/src/pkg 目录中查看这些包。下面简单介绍一些我们开发中常用的包:
- fmt 包实现了格式化的标准输入输出,其中的 fmt.Printf() 和 fmt.Println() 是开发者使用最为频繁的函数
- io包提供了原始的 I/O 操作界面。它主要的任务是对os 包这样的原始的 I/O 进行封装
- bufio 包通过对 io 包的封装,提供了数据缓冲功能,能够一定程度减少大块数据读写带来的开销
- sort 包提供了用于对切片和用户定义的集合进行排序的功能
- strconv 包提供了将字符串转换成基本数据类型,或者从基本数据类型转换为字符串的功能
- os 包提供了不依赖平台的操作系统函数接口,设计像 Unix 风格,但错误处理是 go 风格
- sync 包实现多线程中锁机制以及其他同步互斥机制
- flag 包提供命令行参数的规则定义和传入参数解析的功能。绝大部分的命令行程序都需要用到这个包
- encoding/json 包提供了对 JSON 的基本支持,比如从一个对象序列化为 JSON 字符串,或者从 JSON 字符串反序列化出一个具体的对象等
- html/template包主要实现了 web 开发中生成 html 的 template 的一些函数
- net/http 包提供 HTTP 相关服务,主要包括 http 请求、响应和 URL 的解析,以及基本的 http 客户端和扩展的 http 服务
- reflect 包实现了运行时反射,允许程序通过抽象类型操作对象。
- strings 包主要是处理字符串的一些函数集合,包括合并、查找、分割、比较、后缀检查、索引、大小写处理等等。
- bytes 包提供了对字节切片进行读写操作的一系列函数。
- log 包主要用于在程序中输出日志。log 包中提供了三类日志输出接口,Print、Fatal 和 Panic。
2 Go基本语法
2.0 数据类型
布尔型、 数字类型、字符串类型、派生类型
1 数字类型
类型 | 描述 | 类型 | 描述 |
---|---|---|---|
uint8 | 无符号 8 位整型 (0 到 255) | int8 | 有符号 8 位整型 (-128 到 127) |
uint16 | 无符号 16 位整型 (0 到 65535) | int16 | 有符号 16 位整型 (-32768 到 32767) |
uint32 | 无符号 32 位整型 (0 到 4294967295) | int32 | 有符号 32 位整型 (-2147483648 到 2147483647) |
uint64 | 无符号 64 位整型 (0 到 18446744073709551615) | int64 | 有符号 64 位整型 (-9223372036854775808 到 9223372036854775807) |
2 浮点型
类型 | 描述 |
---|---|
float32 | IEEE-754 32位浮点型数 |
float64(默认) | IEEE-754 64位浮点型数 |
complex64 | 32 位实数和虚数 |
complex128 | 64 位实数和虚数 |
3 其他数字类型
类型 | 描述 |
---|---|
byte | 无符号 8 位整型 (0 到 255),其实就是int8 |
rune | 其实就是int32的别名 |
uint | 32 位或64位 |
int(默认) | 32 位或64位 |
uintptr | 无符号整形,用于存放一个指针 |
2.1 变量 var
定义或声明变量显然会有多种写法,只展示一些常用的,最好能记下来最佳实践(最重要)
1 单变量声明:
// 方式一:声明并初始化
var v_naem v_type = v_value
// 方式二:先声明再初始化,如果没有初始化,则默认为该类型“零值”
var v_naem v_type
v_naem = v_value
// 方式三:根据值自行判定变量类型
var v_naem = v_value
// 方式四:初次声明并初始化(推荐)
v_naem := v_value
2 多变量声明:
// 1 声明全局变量
var (
vname1 v_type1
vname2 v_type2
)
// 2 类型相同的多个变量
var vname1, vname2, vname3 v_type
vname1, vname2, vname3 = v1, v2, v3
// 不需要显示声明类型,自动推断类型(不能声明全局?)
var vname1, vname2, vname3 = v1, v2, v3
// := 的形式声明初始化变量(推荐)
vname1, vname2, vname3 := v1, v2, v3
3 变量声明需要注意的问题
- 局部变量(函数体中的变量)声明之后必须使用(不使用会报错,有点奇怪,Java里面只会wrong)
- 全局变量声明后可以不使用
- := 的方式初始化声明时,该变量不能提前被声明了(会报错)
4 空白标识符的应用
// 交换两变量的值 就是java中一直要手写的swap函数
a, b = b, a
// 空白标识符:抛弃某些你不需要的值
_, b = 5, 7 // 5,7两个数,只要7不要5,就用这种方式保存7的值到b变量中
/*
很典型的应用就是函数返回值有多个时只取用部分返回值,代码如下:
*/
func main() {
_,numb,strs := numbers() //只获取函数返回值的后两个
fmt.Println(numb,strs)
}
//一个可以返回多个值的函数
func numbers()(int,int,string){
a , b , c := 1 , 2 , "str"
return a,b,c
}
5 值类型和引用类型(似乎和java是类似的)
值类型:
- int、float、bool 和 string 这些基本类型都属于值类型,使用这些类型的变量直接指向存在内存中的值。
- 当使用等号
=
将一个变量的值赋值给另一个变量时,如:j = i
,实际上是在内存中将 i 的值进行了拷贝
引用类型
- 一个引用类型的变量 r1 存储的是 r1 的值所在的内存地址(数字),或内存地址中第一个字所在的位置。
- 当使用赋值语句 r2 = r1 时,只有引用(地址)被复制。你可以通过 &i 来获取变量 i 的内存地址,例如:0xf840000040(每次的地址都可能不一样)
- 如果 r1 的值被改变了,那么这个值的所有引用都会指向被修改后的内容,在这个例子中,r2 也会受到影响。
2.2 常量 const
回顾一下java中的final关键字
- final修饰【基本数据类型】成员变量时,表示这个变量的值不能改变
- final修饰【引用数据类型】成员变量时,表示这个这个引用的地址的值不能修改,但是这个引用所指向的对象里面的内容还是可以改变的
1 const常量
Go的常量const是属于编译时期的常量,即在编译时期就可以完全确定取值的常量。
Go中const的用法和var很像,但是常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。
常量如果没有指定类型,则默认为无类型untyped,无类型的数值常量将由big包提供支持,能够对“大数”做运算,但是不能直接打印,打印会有溢出错误。
// 单变量
const c_name c_type = c_value
const c_name = c_value
// 多变量
const c_name1, c_name2 = c_value1, c_value2
const (
c_name1 = c_value1
c_name2 = c_value2
c_name3 = c_value3
)
2 iota特殊常量
iota,特殊常量,可以认为是一个可以被编译器修改的常量。
iota 在 const关键字出现时将被重置为 0(const 内部的第一行之前),const 中每新增一行常量声明将使 iota 计数一次(iota 可理解为 const 语句块中的行索引)。
package main
import "fmt"
func main() {
const (
a = iota //0
b //1
c //2
d = "ha" //独立值,iota += 1
e //"ha" iota += 1
f = 100 //iota +=1
g //100 iota +=1
h = iota //7,恢复计数
i //8
)
fmt.Println(a,b,c,d,e,f,g,h,i)
}
// 打印结果为:0 1 2 ha ha 100 100 7 8
2.3 运算符
运算符包括:算术运算符、关系运算符、逻辑运算符、位运算符、赋值运算符、其他运算符
仔细看了一下几乎所有运算符的用法和Java都一样,这里就介绍一下位运算符、其他运算符
1 位运算符
下表列出了位运算符 &, |, 和 ^ 的计算:
p | q | p & q | p l q | p ^ q |
---|---|---|---|---|
0 | 0 | 0 | 0 | 0 |
0 | 1 | 0 | 1 | 1 |
1 | 1 | 1 | 1 | 0 |
1 | 0 | 0 | 1 | 1 |
假定 A = 60; B = 13; 其二进制数转换为:
A = 0011 1100
B = 0000 1101
-----------------
A&B = 0000 1100
A|B = 0011 1101
A^B = 0011 0001
A << 2 = 1111 0000 //高位丢弃,低位补0
A >> 2 = 0000 1111 //低位丢弃,高位补0
2 其他运算符
下表列出了Go语言的其他运算符。
运算符 | 描述 | 实例 |
---|---|---|
& | 返回变量存储地址 | &a; 将给出变量的实际地址。 |
* | 指针变量。 | *a; 是一个指针变量 |
以下实例演示了其他运算符的用法:
可以这么理解:
- ptr 就是指针变量,表示地址的值
- *ptr 表示指向的对象的值
- 空指针的值为0
package main
import "fmt"
func main() {
var a int = 4
var b int32
var c float32
var ptr *int
/* 运算符实例 */
fmt.Printf("第 1 行 - a 变量类型为 = %T\n", a );
fmt.Printf("第 2 行 - b 变量类型为 = %T\n", b );
fmt.Printf("第 3 行 - c 变量类型为 = %T\n", c );
/* & 和 * 运算符实例 */
ptr = &a /* 'ptr' 包含了 'a' 变量的地址 */
fmt.Printf("a 的值为 %d\n", a);
fmt.Printf("*ptr 为 %d\n", *ptr);
fmt.Printf("ptr 为 %d\n", ptr);
}
以上实例运行结果:
第 1 行 - a 变量类型为 = int
第 2 行 - b 变量类型为 = int32
第 3 行 - c 变量类型为 = float32
a 的值为 4
*ptr 为 4
ptr 为 824633802920
3 运算符的优先级
有些运算符拥有较高的优先级,二元运算符的运算方向均是从左至右。下表列出了所有运算符以及它们的优先级,由上至下代表优先级由高到低:
优先级 | 运算符 | ||
---|---|---|---|
5 | * / % << >> & &^ | ||
4 | + - \ | ^ | |
3 | == != < <= > >= | ||
2 | && | ||
1 | \ | \ |
2.4 条件语句和循环语句
if else语句: 由一个布尔表达式后紧跟一个或多个语句组成
switch语句:用于基于不同条件执行不同动作。
select语句:是select会随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。
for循环,居然没有while
1 if语句
和java没区别,判断语句不需要加括号
package main
import"fmt"
func main(){
var a int
var b int
fmt.Printf("请输入密码: \n")
fmt.Scan(&a)
if a == 5211314 {
fmt.Printf("请再次输入密码:")
fmt.Scan(&b)
if b == 5211314 {
fmt.Printf("密码正确,门锁已打开")
}else{
fmt.Printf("非法入侵,已自动报警")
}
}else{
fmt.Printf("非法入侵,已自动报警")
}
}
2 switch语句
说实话,java中我switch也用的很少,但其实很有必要
相比java,每个case中不需要有break了
package main
import "fmt"
func main() {
/* 定义局部变量 */
var grade string = "B"
var marks int = 90
switch marks {
case 90: grade = "A" //case处也可以用 == 做判断语句:marks==90
case 80: grade = "B"
//fallthrough
case 50,60,70 : grade = "C"
default: grade = "D" //没有执行时的默认选项
}
fmt.Printf("你的等级是 %s\n", grade );
}
/*
fallthrough:使用 fallthrough 会强制执行后面的 case 语句,fallthrough 不会判断下一条 case 的表达式结果是否为 true。
*/
3 for循环
1 基本用法和java一致(不需要小括号)
2 也可以省略init和post参数,做到和while一样的效果
3 range用法类似foreach,可以实现对字符串、数组、切片等的迭代访问
package main
import "fmt"
func main() {
// 基本用法
sum := 0
for i := 0; i <= 3; i++ {
sum += i
}
fmt.Println(sum)
// 2 这样写也可以,更像 While 语句形式
j := 0
sum2 :=0
for j <= 3 {
sum2 += j
j++;
}
fmt.Println(sum2)
// range遍历数组
strings := []string{"google", "runoob"}
for i, s := range strings {
fmt.Println(i, s)
}
}
/*
6
6
0 google
1 runoob
*/
4 控制语句
多了一个 goto的控制语句!!
break、continue的用法一致
GO 语言支持以下几种循环控制语句:
控制语句 | 描述 |
---|---|
break 语句 | 经常用于中断当前 for 循环或跳出 switch 语句 |
continue 语句 | 跳过当前循环的剩余语句,然后继续进行下一轮循环。 |
goto 语句 | 将控制转移到被标记的语句。 |
goto语句,可以跳转,直达被标记的语句
在变量 a 等于 15 的时候跳过本次循环并回到循环的开始语句 LOOP 处:
package main
import "fmt"
func main() {
/* 定义局部变量 */
var a int = 10
/* 循环 */
LOOP: for a < 20 {
if a == 15 {
/* 跳过迭代 */
a = a++
goto LOOP
}
fmt.Printf("a的值为 : %d\n", a)
a++
}
}
以上实例执行结果为:
a的值为 : 10
a的值为 : 11
a的值为 : 12
a的值为 : 13
a的值为 : 14 //没有15!!
a的值为 : 16
a的值为 : 17
a的值为 : 18
a的值为 : 19
2.5 指针
a := 4 ptr *int := &a
指针其实只需要分清楚两个概念:ptr和*ptr
- ptr 就是指针变量,表示地址的值
- *ptr 表示指向的对象的值,称为解引用。(不能解引用nil)
1 指针数组
就是有一个数组a[],一个指针数组ptr[],指针数组的每个元素指向数组a[]的每个元素
2 指向指针的指针
如题意
var ptr **int; //指针的指针
3 指针作为函数参数
这里就要注意是值传递和引用传递!!
这两个概念java中有区分,其实Go语言也一样
将指针变量x *int, y *int
作为函数传递的参数,其实既可以值传递又可以引用传递,主要看你再函数体(方法体)中如何使用这个指针:
- 1 使用*ptr,即使用指向的对象。(此时类似引用传递,对象会改变)
- 2 使用ptr,即指针变量,表示地址的值(此时类似值传递,对象不会改变)
package main
import "fmt"
func main() {
/* 定义局部变量 */
a, b, c, d := 1, 2, 3, 4
fmt.Printf("交换前 a 的值 : %d\n", a)
fmt.Printf("交换前 b 的值 : %d\n", b)
swapForPtr(&a, &b)
fmt.Printf("交换后 a 的值 : %d\n", a)
fmt.Printf("交换后 b 的值 : %d\n", b)
fmt.Println("---------------")
fmt.Printf("交换前 c 的值 : %d\n", c)
fmt.Printf("交换前 d 的值 : %d\n", d)
swapForValue(&c, &d)
fmt.Printf("交换后 c 的值 : %d\n", c)
fmt.Printf("交换后 d 的值 : %d\n", d)
}
func swapForPtr(x *int, y *int) {
*x, *y = *y, *x
}
func swapForValue(x *int, y *int) {
x, y = y, x
}
a、b之间交换元素,使用*x *y
传递,对象发生了改变;c、d之间交换元素,使用x y传递
,对象没有变:
交换前 a 的值 : 1
交换前 b 的值 : 2
交换后 a 的值 : 2
交换后 b 的值 : 1
---------------
交换前 c 的值 : 3
交换前 d 的值 : 4
交换后 c 的值 : 3
交换后 d 的值 : 4
2.6 结构体
结构体将相关的值组合在一起,使传递时更不容易出错
1 结构体定义需要使用 type 和 struct 语句:
- type 语句设定了结构体的名称。
- struct 语句定义一个新的数据类型,结构体中有一个或多个成员。
// 定义一个书籍结构体
type Book struct {
title string
author string
subject string
book_id int
}
func main() {
// 定义一个Book变量,类似于用构造函数new一个对象
book := Book{"Go 语言1", "www.runoob.com", "Go语言教程1", 6495407}
fmt.Println(book)
fmt.Println(book.title) // 访问Book变量的属性
// 也可以使用 key => value 格式定义变量
book2 := Book{title: "Go 语言2", author: "www.runoob.com", subject: "Go语言教程2", bookId: 6495407}
fmt.Println(book2)
fmt.Println(book2.title) // 访问Book变量的属性
// 使用key => value 格式,忽略的字段为 0 或 空
book3 := Book{title: "Go 语言3", author: "www.runoob.com"}
book3.subject = "Go语言教程3" //可以向set一样设置属性
fmt.Println(book3)
fmt.Println(book3.title) // 访问Book变量的属性
}
打印结果如下(我还是觉得java的new更优雅):
{Go 语言1 www.runoob.com Go语言教程1 6495407}
Go 语言1
{Go 语言2 www.runoob.com Go语言教程2 6495407}
Go 语言2
{Go 语言3 www.runoob.com Go语言教程3 0}
Go 语言3
2 指向结构体的指针
其实就是变量变成了自定义的结构体类型
// 定义一个指向结构体变量的指针
var ptr *Book = &book
fmt.Println(ptr.title) //可以直接用指针对象访问真实对象的元素
fmt.Println(ptr) //返回地址,但不显示实际物理地址,显示&{对象}
fmt.Println(*ptr) //返回指向对象的值
打印显示:
Go 语言1
&{Go 语言1 www.runoob.com Go语言教程1 6495407}
{Go 语言1 www.runoob.com Go语言教程1 6495407}
3 Go容器
数组、切片、Map和List
3.1 数组
需要注意,go中数组默认是值传递:直接建立副本,不会影响原数组。(但效率很低)
当然也可以通过指针传递地址的值,比如
swap(arr *[]int)
但通常,函数会使用切片作为形参传值,此时就是传递引用地址的值
1 声明或初始化数组
// 定义数组并初始化 可以用[...]代替[5]
nums := [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0,}
// 指定特定下标的初始化:将索引为 1 和 3 的元素初始化
nums := [5]float32{1:2.0, 3:7.0}
// 指定长度的空数组,需要使用make来定义
nums := make([]int, len)
// 已知长度,该长度必须是常量,如
// 定义数组必须指定长度,默认值为0【最常用】
nums := [5]float32{}
var nums [5]float32
//定义空数组,,即长度为0,是不能赋值操作的,只能append拼接
var nums []float32 //这tm其实是切片
2 二维或多维数组
//1 将两个一维切片append进二维数组,可以不等长!!!
row1 := []int{1, 2, 3} //没有指定长度 统统视为切片
row2 := []int{4, 5, 6, 7} //没有指定长度 统统视为切片
values = append(values, row1)
values = append(values, row2)
//2 也可以直接指定初始化数组
a := [3][4]int{
{0, 1, 2, 3} , /* 第一行索引为 0 */
{4, 5, 6, 7} , /* 第二行索引为 1 */
{8, 9, 10, 11}} /* 第三行索引为 2 */
//3 创建数组,并循环遍历数组赋值【最常用】
var a = [5][2]int{}
var i, j int
/* 数组元素赋值 */
for i = 0; i < 5; i++ {
for j = 0; j < 2; j++ {
a[i][j] = i+j;
}
}
// 求行的长度rows, 第一行的长度cols(列数)
if len(matrix) == 0 {return false}
rows, cols := len(matrix), len(matrix[0])
3.2 切片
切片(slice)是对数组的一个连续片段的引用。
nums := make([]type,len,cap)
支持len()、cap()、截取[a:b]、append()、copy()
切片(slice)是对数组的一个连续片段的引用,所以切片是一个引用类型(因此更类似于 C/C++中的数组类型,或者 Python 中的 list 类型)。用索引表示数组的一部分时需注意终止索引标识的项不在切片内。
Go语言中切片的内部结构包含地址、大小和容量。
1 三种创建切片的方式
- 1 声明一个未指定大小的数组来定义切片
- 2 使用 make() 函数来创建切片
- 3 截取数组的片段作为切片
package main
import "fmt"
func main() {
// 1 声明一个未指定大小的数组来定义切片
var s1 []int
var s11 []int{1,2,3,4}
printSlice(s1)
printSlice(s11)
//2 使用 make() 函数来创建切片
s2 := make([]int, 2, 5)
printSlice(s2)
//3 截取数组的片段作为切片
arr := [5]int{1, 2, 3, 4, 5}
s3 := arr[2:4] //只取到2 3索引位置的值,和java中substring()很像
printSlice(s3)
}
func printSlice(s []int) {
len1 := len(s)
cap1 := cap(s)
fmt.Printf("切片:%v,len:%d,cap:%d \n", s, len1, cap1)
}
打印内容:
切片:[],len:0,cap:0
切片:[1 2 3 4],len:4,cap:4
切片:[0 0],len:2,cap:5
切片:[3 4],len:2,cap:3
2 append() 和 copy()函数
append() 拼接两个切片为一个更大的切片
copy() 复制切片,要注意len和cap
append和copy的组合可以实现一系列对切片的操作功能!!(后面自己摸索)
append是切片的内置函数,两种使用方式:
- s :=append(s,1,2,3,4)
- s :=append(s,s1...)
如果append后原数组容量放不下所有元素,则会新开辟数组给切片引用。但如果原数组容量足够,那么操作切片将改变原数组。
// s2 [0 0]
// s3 [3 4]
sAll1 := append(s2, s3...) //使用append合并两切片,必须加上...
printSlice(sAll1)
sAll2 := append(sAll1, 7, 8)
printSlice(sAll2)
var sAll3 = make([]int, 6, 10) //注意拷贝对象的len和cap最好大于原对象
copy(sAll3, sAll2) //拷贝sAll2到sAll3
printSlice(sAll3)
打印:
切片:[0 0 3 4],len:4,cap:5
切片:[0 0 3 4 7 8],len:6,cap:10
切片:[0 0 3 4 7 8],len:6,cap:8
3.3 Map集合
Map 是一种无序的键值对的集合。Map的特点是通过 key 来快速检索数据
1 Map
可以使用内建函数 make 也可以使用 map 关键字来定义 Map:
/* 声明变量,默认 map 是 nil 无法存键值对 */
var map1 map[string]int //一般不用
/* 1使用 make函数,可以预先指定容量 或者 2类似空数组的方式 */
map1 := make(map[string]int,size)
map1 := map[string]int{}
如果不初始化 map,那么就会创建一个 nil map。
package main
import "fmt"
func main() {
map1 := make(map[string]int,4)
map1["wukang"] = 1
map1["wukang"] = 2 // 相同的key会覆盖上一个value
map1["wukang3"] = 3
map1["wukang4"] = 4
fmt.Println(map1)
value, isExit := map1["wukang"]
fmt.Println(value, isExit)
// 无序的 每次遍历的顺序不一样
for key, value := range map1 {
fmt.Println(key, ",", value)
}
// delete 参数元素
delete(map1, "wukang2")
_, isExit2 := map1["wukang2"]
fmt.Println(isExit2)
}
/*
map[wukang:2 wukang3:3 wukang4:4]
2 true
wukang4 , 4
wukang , 2
wukang3 , 3
false
*/
2 sync.Map
暂时还不太理解,后面还有再多看看
sync.Map 有以下特性:
- 无须初始化,直接声明即可。
- sync.Map 不能使用 map 的方式进行取值和设置等操作,而是使用 sync.Map 的方法进行调用,Store 表示存储,Load 表示获取,Delete 表示删除。
- 使用 Range 配合一个回调函数进行遍历操作,通过回调函数返回内部遍历出来的值,Range 参数中回调函数的返回值在需要继续迭代遍历时,返回 true,终止迭代遍历时,返回 false。
package main
import (
"fmt"
"sync"
)
func main() {
var syncMap sync.Map
syncMap.Store("wukang", 1)
syncMap.Store("wukang2", 2)
syncMap.Store("wukang2", 22)
syncMap.Store("wukang3", 3)
// 遍历
syncMap.Range(func(key, value interface{}) bool {
fmt.Println(key, value)
return true
})
}
/*
wukang 1
wukang2 22
wukang3 3
*/
3.4 List集合
container/list包下,本质是一个双向链表
通过定义句柄的方式,来实现指定位置的添加和删除
基本命令如下:
// 初始化列表 方式一:
listName := list.New()
// 初始化列表 方式二:
var listName list.List
// 添加元素 Push
listName.PushBack(v interface {})
listName.PushFront(v interface {})
// 添加元素 Insert(mark是指定的句柄)
listName.InsertAfter(v interface {}, mark * Element)
listName.InsertBefore(v interface {}, mark * Element)
// 添加列表 PushList
listName.PushBackList(other *List)
listName.PushFrontList(other *List)
// 删除元素
listName.Remove(mark * Element)
// 移动元素
listName.MoveToFront(e *Element) //移动元素到链表第一个位置
listName.MoveToBack(e *Element) //移动元素到链表最后一个位置
listName.MoveBefore(e, mark *Element) //移动元素e到mark位置的前面
listName.MoveAfter(e, mark *Element) //移动元素e到mark位置的后面
// 其他命令
listName.Len()
e.Next() //返回下一个元素或者nil
e.Prev() //返回前一个元素或者nil
listName.Front() //返回第一个元素或者nil
listName.Back() //返回最后一个元素或者nil
实例测试:
package main
import (
"container/list"
"fmt"
)
func main() {
listName := list.New()
listName.PushBack("Kang")
listName.PushFront("Wu")
listName.PushBack("Zui")
listName.PushBack("Shuai")
listName.PushBack(1)
element := listName.Back()
fmt.Println("最后一个元素:", element.Value)
listName.Remove(element)
// 遍历list集合的元素
for elem := listName.Front(); elem != nil; elem = elem.Next() {
fmt.Println(elem.Value)
}
}
/*
最后一个元素: 1
Wu
Kang
Zui
Shuai
*/
4 状态和行为
4.1 函数和方法
Java里面因为有类和对象的概念,所以函数和方法的概念是一样的。
go不提供类,只有包和数据类型的区分。函数指某个包的独立的函数,方法指某个类型有关的方法
参考了《Go语言趣学指南》的第3单元和第5单元
函数和方法的声明都是用func关键字,但格式不同:
- 函数是某个包下的函数,如果首字母大写表示,外部包也可以调用
- 方法必须是某一类型的方法,由该类型的变量调用
//函数声明:
fun Swap(nums *int[], i int, j int) bool{
temp := nums[i]
nums[i] = nums[j]
nums[j] = temp
return true
}
//调用方式
flag := XXX.Swap(*arr,1,2) //XXX表示包名,Swap函数首字母大写表示可以在外部直接调用
//方法声明:接受参数写在前面,方法必须是某个类型的方法
// func关键字后面的类型 (w world)指定了该方法可以被那个类型的变量调用
type world float64
func (w world) swap() 输出类型{
...
}
//调用方式
w := 10.0
outValue := w.swap() //使用该类型的变量调用方法
go语言中,方法是跟特定类型相关联的函数。一般我们可以通过type关键字自定义类型让代码变得更易读且更可靠。go中类型下的方法同样可以重载,即方法名相同但参数不同。
4.2 一等函数
go语言中,函数可以当作一个变量使用,作为形参。之前没有见过
1 函数可以赋值给变量var,利用该变量直接调用该函数
// 声明一个函数,获取传感器温度
func fakeSensor() int {
return rand.Intn(151) + 150
}
func main() {
// 1 直接将函数赋值给变量,自动类型判断
sensor := fakeSensor
// 2 先声明类型 再赋值
var sensor2 func() int
sensor2 = fakeSensor
// 利用变量调用函数
fmt.Println(sensor())
fmt.Println(sensor2())
}
2 函数本身也可以作为一种类型,名之为“声明函数类型”
声明函数类型的特征是:函数名、输入参数以及输出参数,也就是说三者一致可以重复赋值
// 声明函数类型的格式
type typeName func(输入参数 类型) 输出类型
// 举例
type addFunc func(a int, b int) int
当函数作为一种类型时,就可以将该函数作为别的函数的形参或者返回值
func otherFunc(c int, f(a,b) addFun) bool
3 匿名函数和闭包
- 匿名函数就是没有名称的函数,直接调用,在go中也称为函数字面量。函数字面量可以保留外部作用域的变量引用。
- 带有闭包特性的匿名函数可以更方便的动态创建函数
// 方式一:声明匿名函数,然后调用
func main() {
// 定义匿名函数并赋值给变量f
f := func(message string) {
fmt.Println(message)
}
f("Go to the party.") //利用变量调用函数
}
//方式二:声明并调用匿名函数
func main() {
func() {
fmt.Println("Functions anonymous")
}() //括号表示调用,无参数输入
}
闭包指由匿名函数封闭并包围作用域中的变量。需要注意闭包保留的都是外围变量的引用而不是副本。即:匿名函数能够保留外部作用域的变量引用
func main() {
a := 1
nm := func() int {
return a //匿名函数可以调用函数作用域外面的变量
}
fmt.Println(nm())
a++ // 打印值会发生变化,说明是引用传递
fmt.Println(nm())
}
/*
1
2
*/
4.3 面向对象设计
Go不支持类和对象,也没有继承。但Go提供了结构和类型的方法,通过组合这两者可以实现面向对象的设计。
1 结构体的方法
Go语言没有类和对象的概念,但可以通过定义结构体(可以认为是一种类型),并为该结构体绑定方法,实现类相似的功能。一般可以为自定义的结构体类型,绑定构造方法或其他普通方法。
Go语言没有为构造器提供特殊的语言特性,仅选择了newType或者NewType的函数用于构造指定类型的值,构造器仅是一种遵循命名惯例的普通函数。
为coordinate结构体类型指定方法,以及为location指定构造器如下:
package main
import "fmt"
// location结构体
type location struct {
lat, long float64
}
// coordinate结构体
type coordinate struct {
d, m, s float64
h rune
}
// newLocation构造器
func newLocation(lat, long coordinate) location {
//通过coordinate变量调用了decimal方法
//然后通过结构体字面量生成location结构体
return location{lat.decimal(), long.decimal()}
}
// decimal是coordinate类型的方法
func (c coordinate) decimal() float64 {
sign := 1.0
switch c.h {
case 'S', 'W', 's', 'w': //南纬或者西经变负号
sign = -1
}
return sign * (c.d + c.m/60 + c.s/3600)
}
func main() {
coord1 := coordinate{4, 35, 22.2, 'S'}
coord2 := coordinate{137, 26, 30.12, 'E'}
curiosity := newLocation(coord1, coord2) //调用构造器
fmt.Println(curiosity)
}
为类型定义方法,可以实现方法的复用,有点面向对象的思想。比如下面的代码:将distance定义为world类型的方法,那么就获得了在不同世界(星球)计算位置间距离的方法,并且该方法还相当简洁,因为distance方法可以直接访问世界的半径(通过mars调用)。
package main
import (
"fmt"
"math"
)
// location结构体
type location struct {
lat, long float64
}
// world结构体
type world struct {
radius float64
}
// 这是world这个类型的方法,计算两点之间的距离
func (w world) distance(p1, p2 location) float64 {
s1, c1 := math.Sincos(rad(p1.lat))
s2, c2 := math.Sincos(rad(p2.lat))
clong := math.Cos(rad(p1.long - p2.long))
return w.radius * math.Acos(s1*s2+c1*c2*clong)
}
// 这是一个普通函数
func rad(deg float64) float64 {
return deg * math.Pi / 180
}
var mars = world{radius: 3389.5} //定义一个world类型的结构体变量mars
func main() {
spirit := location{-14.5684, 175.472636}
opportunity := location{-1.9462, 354.4734}
dist := mars.distance(spirit, opportunity) //调用方法,体现复用性
fmt.Printf("%.2f km\n", dist)
}
2 组合与转发(嵌入)
Go里面是没有继承的,“事实上对传统继承的使用并不是必须的,所有使用继承解决的问题都可以通过其他方法解决”,Go甩掉了继承这种老旧范式的包袱
Go可以通过结构体的方式实现组合,即:通过结构体中嵌套其他自定义的结构体类型的方式,将小型结构组合成为一个大型的结构体。
其实就是把自定义的结构体看作是一种类型,然后可以基于这些结构体定义新的结构体。更重要的是,新的结构体可以转发使用其内部结构体的方法。
package main
import "fmt"
type temperature struct {
high, low celsius
}
type location struct {
lat, long float64
}
type report struct {
sol int
location location //location作为类型
temperature temperature //temperature作为类型
}
type celsius float64
//temperature类型的average方法
func (t temperature) average() celsius {
return (t.high + t.low) / 2
}
// report大结构体手动转发使用temperature的average方法
func (r report) average() celsius {
return r.temperature.average()
}
func main() {
//t使用自己的方法
t := temperature{high: -1.0, low: -78.0}
fmt.Printf("average %vº C\n", t.average())
//report使用转发的方法
report := report{sol: 15, temperature: t}
fmt.Printf("average %vº C\n", report.temperature.average())
}
/*
average -39.5º C
average -39.5º C
*/
大结构体要使用小结构体的方法每次都要手动转发,未免太过繁琐。可以通过“类型嵌入”的方式实现方法的自动转发。具体方式为:声明大结构体时不给定内置结构体的字段名,结构体会自动为被嵌入的类型生成同名的字段,嵌入不仅可以转发方法,还能让外部结构直接访问内部结构中的字段。
package main
import "fmt"
// 嵌入三个类型/结构体,不指定字段名
type report struct {
sol
location
temperature
}
type sol int
type temperature struct {
high, low celsius
}
type location struct {
lat, long float64
}
type celsius float64
func (s sol) days(s2 sol) int {
days := int(s2 - s)
if days < 0 {
days = -days
}
return days
}
func main() {
report := report{sol: 15}
fmt.Println(report.sol.days(1446))
fmt.Println(report.days(1446)) //可以直接调用访问内部结构体的方法
}
需要注意的是,当两个内置结构体具有相同名称的方法时,采用自动转发的方式会发生命名冲突,解决方案是手动转发指定的嵌入方法。如下:
//比如location类型也有days方法
func (l location) days(l2 location) int {
return 5
}
// 对于report而言,就需要手动指定需要的days方法
func (r report) days(s2 sol) int {
return r.sol.days(s2)
//return r.location.days(s2)
}
func main() {
report := report{sol: 15}
d := report.days(1446) //手动指定之后才调用才不会报错
fmt.Println(d)
}
3 接口
普通的类型关注于储存了什么值,而接口类型关注于“类型可以做什么”。普通类型通过方法来表达自己的行为,而接口则是通过列举类型必须满足的一组方法来声明。
关注物件的行为而不是它们构成的本身,通过接口进行表述的思维方式可以让代码更易于适应变化。
通常会将接口声明为类型,并为其命名。接口类型的名称按惯例需要以-er为后缀,接口类型可以用在其他类型能够使用的任何地方。
package main
import (
"fmt"
"strings"
)
// 定义一个talker接口
// 只要声明了一个名为talk()的方法、不接受实参并返回字符串,就表示满足了该接口的要求
type talker interface {
talk() string
}
type martian struct{}
// martian类型实现了talker接口
func (m martian) talk() string {
return "nack nack"
}
type laser int
// laser类型实现了talker接口
func (l laser) talk() string {
return strings.Repeat("pew ", int(l))
}
// 将接口作为普通函数的形参,功能是小写变大写
func shout(t talker) {
louder := strings.ToUpper(t.talk())
fmt.Println(louder)
}
func main() {
//接口在修改代码和扩展代码的时候能发挥其灵活性。只要声明了一个带有talk方法的新类型,那么shout函数将自动适用于它。
shout(martian{}) //martian实现接口
shout(laser(2)) //laser实现接口,这里还有类型转换
type crater struct{}
// crater does not implement talker (missing talk method)
// shout(crater{})
}
/*
NACK NACK
PEW PEW
*/
可以同时使用组合(嵌入)和接口,某个类型实现了接口,则将该类型嵌入的结构体类型也等同于实现了接口,可以直接调用接口方法。
另外Go语言中,可以随时创建新的接口,任何代码包括已经实现的代码都可以实现该接口(比如标准库中的代码),也成为隐式满足接口,目的是可以让代码更灵活。
package main
import (
"fmt"
"strings"
)
// 定义talker接口 含一个talk()方法
type talker interface {
talk() string
}
type laser int
// laser类型实现talker接口
func (l laser) talk() string {
return strings.Repeat("pew ", int(l))
}
func shout(t talker) {
louder := strings.ToUpper(t.talk())
fmt.Println(louder)
}
func main() {
// 定义结构体,组合嵌入laser类型
type starship struct {
laser
}
s := starship{laser(3)}
fmt.Println(s.talk()) //大接口可以直接调用接口方法
shout(s)
}
5 Go并发
Go语言在 GOMAXPROCS 数量与任务数量相等时,可以做到并行执行,但一般情况下都是并发执行。
参考[2]
5.1 为并发而生
Go 语言的并发通过 goroutine 特性完成。goroutine 类似于线程,但并非线程。goroutine 是由 Go 语言的运行时调度完成,而线程是由操作系统调度完成。
Go语言从底层原生支持并发,无须第三方库,开发人员可以很轻松地在编写程序时决定怎么使用 CPU 资源。
Go语言的并发是基于 goroutine 的,可以将 goroutine 理解为一种虚拟线程。Go语言运行时会参与调度 goroutine,并将 goroutine 合理地分配到每个 CPU 中,最大限度地使用 CPU 性能。
多个 goroutine 中,Go语言使用通道(channel)进行通信,通道是一种内置的数据结构,可以让用户在不同的 goroutine 之间同步发送具有类型的消息。这让编程模型更倾向于在 goroutine 之间发送消息(通道),而不是让多个 goroutine 争夺同一个数据的使用权(共享内存)。
程序可以将需要并发的环节设计为生产者模式和消费者的模式,将数据放入通道。通道另外一端的代码将这些数据进行并发计算并返回结果,如下图所示。
1 goroutine
使用 go 关键字就可以创建 goroutine,将 go 声明放到一个需调用的函数之前,在相同地址空间调用运行这个函数,这样该函数执行时便会作为一个独立的并发线程,这种线程在Go语言中则被称为 goroutine。三种实现方式如下:
//1 go 关键字放在方法调用前新建一个 goroutine 并执行方法体
go GetThingDone(param1, param2);
//2 新建一个匿名方法并执行
go func(param1, param2) {
}(val1, val2)
//3 直接新建一个 goroutine 并在 goroutine 中执行代码块
go {
//do someting...
}
2 channel
channel 是进程内的通信方式,因此通过 channel 传递对象的过程和调用函数时的参数传递行为比较一致,比如也可以传递指针等。如果需要跨进程通信,我们建议用分布式系统的方法来解决,(比如使用 Socket 或者 HTTP )。
channel 是类型相关的,也就是说,一个 channel 只能传递一种类型的值,这个类型需要在声明 channel 时指定。
Go语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。
- 声明或创建通道
var 通道变量 chan 通道类型 //声明通道类型
var chanName chan float32
通道实例 := make(chan 数据类型) //创建通道
ch1 := make(chan int)
ch2 := make(chan interface{})
type Equip struct{ /* 一些字段 */ }
ch3 := make(chan *Equip) //创建Equip指针类型的通道, 可以存放*Equip
- 向通道发送数据、接受数据
// 向通道发送数据:通道变量 <- 值
ch1 := make(chan int)
ch1 <- 0 //只发送一个0
ch1 <- 100
// 从通道接受数据:值 <- 通道变量
num := <- ch1
num2, isGet := <- ch1 //占用CPU高 不推荐使用
<- ch1 //忽略或丢弃通道中的数据
5.2 Goroutine和Channel示例
以打瞌睡的地鼠为例,展示Go和Chan的示例
- 1 五只打瞌睡的地鼠
package main
import (
"fmt"
"time"
)
func main() {
for i := 0; i < 5; i++ {
go sleepyGopher(i)
}
time.Sleep(4 * time.Second)
}
func sleepyGopher(id int) {
time.Sleep(1 * time.Second)
fmt.Println("... ", id, " snore ...")
}
对于goroutine的执行并不是顺序执行,go命令还没结束就立即回执行下一次循环的遍历。同时,多个不同的goroutine是以任意顺序执行的,打印的结果如下:
/*
... 0 snore ...
... 3 snore ...
... 2 snore ...
... 1 snore ...
... 4 snore ...
*/
- 2 通道使用示例:
默认情况下,通道是不带缓冲区的。发送端发送数据,同时必须有接收端相应的接收数据。
ch := make(chan int, 100)
// 设置缓冲区大小为100
package main
import "fmt"
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // 把 sum 发送到通道 c
}
func main() {
s := []int{1, 2, 3, 4, 5, 6}
c := make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c
fmt.Println(x, y)
}
/*
15 6
*/
- 3 地鼠装配线:多个goroutine通过chan传递信息
package main
import (
"fmt"
"strings"
)
func main() {
c0 := make(chan string)
c1 := make(chan string)
go sourceGopher(c0)
go filterGopher(c0, c1)
printGopher(c1)
}
//生产资源的地鼠,将三个字符串放入行参downstream的通道
func sourceGopher(downstream chan string) {
for _, v := range []string{"hello world", "a bad apple", "goodbye all"} {
downstream <- v
}
close(downstream)
}
// 过滤地鼠,过滤掉含bad的字符串
func filterGopher(upstream, downstream chan string) {
// range遍历通道,程序可以在通道倍关闭之前,一直从通道里面读取值
for item := range upstream {
if !strings.Contains(item, "bad") {
downstream <- item
}
}
close(downstream)
}
// 打印地鼠,打印最终的结果
func printGopher(upstream chan string) {
for v := range upstream {
fmt.Println(v)
}
}
/*
hello world
goodbye all
*/
5.2 原子函数和互斥锁
两个简单示例,介绍atomic.AddInt64和sync.Mutex.Lock()的使用
Go语言提供了传统的同步 goroutine 的机制,就是对共享资源加锁。atomic 和 sync 包里的一些函数就可以对共享的资源进行加锁操作。
- 1 原子函数能够以很底层的加锁机制来同步访问整型变量和指针
- 2 另一种同步访问共享资源的方式是使用互斥锁。互斥锁用于在代码上创建一个临界区,保证同一时间只有一个 goroutine 可以执行这个临界代码。
介绍原子函数和互斥锁之前,先介绍要用到的三个控制goroutine流程的命令:
runtime.Gosched() //让当前 goroutine 暂停
var wg sync.WaitGroup
/* sync.WaitGroup类型的对象内部有一个计数器,最初从0开始,它有三个方法:Add(), Done(), Wait() 用来控制计数器的数量。
Add(n) 把计数器设置为n ,
Done() 每次把计数器-1 ,
wait() 会阻塞代码的运行,直到计数器地值减为0(即wg==0)才结束阻塞。*/
defer wg.Done() //即先执行这个函数内的其他语句,最后执行wg.Done()命令
/*关键字defer向函数注册退出调用,即主函数退出时,defer后的函数才被调用。defer语句的作用是不管程序是否出现异常,均在函数退出时自动执行相关代码。
当执行到defer时,暂时不执行,会将defer后面的语句压入到独立的(defer栈)
当函数执行完毕后,再从defer栈 安装先入后出的方式出栈执行*/
1 atomic.AddInt64原子函数+1
- 使用
atomic.AddInt64(&counter, 1)
//安全的对counter加1,结果一定是20。
package main
import (
"fmt"
"runtime"
"sync"
)
var (
counter int64
wg sync.WaitGroup
)
func main() {
wg.Add(2)
go incCounter(1)
go incCounter(2)
wg.Wait() //等待goroutine结束
fmt.Println(counter)
}
func incCounter(id int) {
defer wg.Done()
for count := 0; count < 10; count++ {
atomic.AddInt64(&counter, 1) //安全的对counter加1
//counter++ //不安全,结果可能为19可能为20
runtime.Gosched()
}
}
上述代码中使用了 atmoic 包的 AddInt64 函数,这个函数会同步整型值的加法,方法是强制同一时刻只能有一个 gorountie 运行并完成这个加法操作。当 goroutine 试图去调用任何原子函数时,这些 goroutine 都会自动根据所引用的变量做同步处理。
2 互斥锁Lock()
斥锁用于在代码上创建一个临界区,保证同一时间只有一个 goroutine 可以执行这个临界代码。
package main
import (
"fmt"
"runtime"
"sync"
)
var (
counter1 int64
wg1 sync.WaitGroup
mutex sync.Mutex
)
func main() {
wg1.Add(2)
go incCounter1(1)
go incCounter1(2)
wg1.Wait()
fmt.Println(counter1)
}
func incCounter1(id int) {
defer wg1.Done()
for count := 0; count < 10; count++ {
//同一时刻只允许一个goroutine进入这个临界区
mutex.Lock()
{
value := counter1
runtime.Gosched() //虽然当前goroutine暂停了但仍然持有锁
value++
counter1 = value
}
mutex.Unlock() //释放锁,允许其他正在等待的goroutine进入临界区
}
}
同一时刻只有一个 goroutine 可以进入临界区。之后直到调用 Unlock 函数之后,其他 goroutine 才能进去临界区。
当调用 runtime.Gosched 函数强制将当前 goroutine 退出当前线程后,调度器会再次分配这个 goroutine 继续运行。所以结果始终是20