Golang 基础学习
go run main.go
go build && ./main
go语言类似面向对象语言又有区别,没有继承概念,通过函数组合来简化代码开发和复用。
使用vscode开发配置国内源
go env -w GOPROXY=https://goproxy.cn,direct
goland附加进程调试
1、设置断点
2、安装:go get -u github.com/google/gops
3、打包
go build -gcflags=“all=-N -l” -o demo1
goland远程调试
go remote
服务器执行:dlv debug --headless --listen=:2345 --api-version=2 --accept-multiclient
本地配置
dep包管理器
# 安装
brew install dep
mkdir $GOPATH/src/test
cd $GOPATH/src/test
# 初始化后会生成下列3个文件/目录,vendor里的依赖优先加载。
dep init
├── Gopkg.lock # 自动生成的文件
├── Gopkg.toml # 定义直接依赖项
└── vendor # 这个目录的依赖代码是优先加载的,类似 node 的 node_module 目录。
# 依赖管理帮助
dep help ensure
# 添加一条依赖
dep ensure -add github.com/bitly/go-simplejson
# 这里 @= 参数指定的是 某个 tag
dep ensure -add github.com/bitly/go-simplejson@=0.4.3
# 添加后一定记住执行 确保 同步
dep ensure
# 建议使用
dep ensure -v
# 删除没有用到的 package
dep prune -v
变量
变量定义
var varname type = value
var varname1, varname2 type = v1, v2
varname := value (只允许在func函数内定义)
特殊:_(下划线) 任何赋予它的值都会被丢弃,
例:变量varname赋值20成功,10被丢弃
_, varname := 10, 20
常量
所谓常量,也就是在程序编译阶段就确定下来的值,而程序在运行时无法改变该值。
例:
v2 := 10
const i = 1000
i = v2 // 当对常量赋值时,会发现编辑器提示报错
基本数据类型
判断数据类型
fmt.Printf("%T", 1.121) // 打印数据类型
基本数据类型转换
//1、string到int
int, _ := strconv.Atoi(string2)
fmt.Printf("%T %d", int, int)
//2、string到int64
int64, _ := strconv.ParseInt(string2, 10, 64)
fmt.Printf("\n%T", int64)
//
//3、int到string
string := strconv.Itoa(int)
fmt.Printf("\n%T", string)
//
//4、int64到string
stringA := strconv.FormatInt(int64, 10)
fmt.Printf("\n%T", stringA)
//
//5、字符串到float64
var string4 = "haha"
float64, _ := strconv.ParseFloat(string4, 32)
fmt.Printf("\n%T", float64)
//6、int64转int
//intb := int(int64)
//fmt.Printf("%T", intb)
//
//7、int转int64
//int64:=int64(int)
整型
uint:正整数
int:所有整数
var num1 uint
var num2 int
num1 = 255
num2 = -1
fmt.Println(num1, num2)
浮点型
var num3 float64
num3 = 3.141592654
布尔类型
var bool1 bool
bool1 = false
字符串(两种)
- uint8类型( byte 型),代表了ASCII码的一个字符。
- rune类型,代表一个 UTF-8字符(处理中文)。
1.字符串默认不可变,若想变,需要转为为[]byte类型或rune类型
s := "hello"
c := []byte(s) // 英文字符串 s 转换为 []byte 类型(
c[0] = 'c'
s2 := string(c) // 再转换回 string 类型
fmt.Printf("%s\n", s2)
s3 := "12哈哈"
rune2 := []rune(s3) // 中文使用[]rune)
rune2[0] = '哈'
fmt.Println(string(rune2))
2.使用+操作符来连接两个字符串
3.切片操作–同python
4.多行字符串使用 ``
res := 111 222 333
使用strings库处理字符串:https://studygolang.com/articles/5769
例:
s := "11 22"
fmt.Println(strings.Contains(s, "11"))
5.强制类型转换
int()
float64()
string()
array数组与切片
1.必须指定元素的类型
2.[…] 根据初始值自动推断数组长度是多少
数组定义及常用方法
len(arr):获取长度
cap(arr):获取容量
// 数组定义
func main() {
// 必须指定元素的类型和容量
// 长度是数组类型的一部分,a1和a2
var a1 [3]bool // [true false ture]
var a2 [4]bool //
// %T元素类型
fmt.Printf("%T %T\n", a1, a2)
// 数组初始化,默认是零值(布尔:false,整形和浮点型:0,字符串:"")
fmt.Println(a1, a2)
// 初始化1
a1 = [3]bool{true, true, false}
fmt.Println(a1)
// 初始化2: ... 根据初始值自动推断数组长度是多少
a100 := [...]int{1, 1, 3}
fmt.Println(a100)
// 初始化3: 根据索引初始化
a3 := [5]int{0: 1, 4: 2}
fmt.Println(a3)
// 初始化4: new
var d = new([10]int)
d[3] = 100
// 数组是值类型
b1 := [3]int{1, 2, 3}
b2 := b1
b2[0] = 100
fmt.Println(b1, b2, b1)
}
数组操作(转切片)
数组没有append方法,需要先将数组转化为切片,对切片进行操作
func main() {
// 数组没有append方法,需要先将数组转化为切片,对切片进行操作
a := [3]int{0, 1, 2}
cl := a[:]
fmt.Println(cl)
cl = append(cl, 3, 4)
// 当容量(cap)不足时,切片会在上一个数组长度(len)基础上成倍扩充容量
fmt.Println(len(cl), cap(cl)) // 当有5个元素时,此时cap是6,
cl = append(cl, 5, 6)
fmt.Println(cl)
fmt.Println(len(cl), cap(cl)) // 当有7个元素时,超过原来的cap,则此时cap是12
/*
[0 1 2]
5 6
[0 1 2 3 4 5 6]
7 12
*/
}
初始化切片
func main() {
a := [3]int{0, 1, 2}
cl := a[:]
cl1 := a[2:]
fmt.Println(cl1)
cl = append(cl, 10)
fmt.Println(cl)
// copy(dest, src) 将源切片中的元素复制到目标切片中(从index 0 开始覆盖)
copy(cl, cl1)
fmt.Println(cl)
// 1、新建一个空切片
var aa []int
// 2、使用make新建一个切片,必须指定长度
aaa := make([]int, 5)
fmt.Println(aa, aaa)
/*
[2]
[0 1 2 10]
[2 1 2 10] 可以看到从index 0 开始覆盖
[] [0 0 0 0 0]
*/
}
数组遍历
// 数组遍历
citys := [...]string{"beijing", "shanghai", "shenzhen"}
// 1.根据索引遍历
for i := 0; i < len(citys); i++ {
fmt.Println(citys[i])
}
// 2.for range 遍历
for i, v := range citys {
fmt.Println(i, v)
}
// 2维数组定义[[1,2] [3,4] [5,6]],
// 最外层可以写...,即a11 = [...][2]int{}
var a11 [3][2]int
a11 = [3][2]int{
[2]int{1, 2},
[2]int{3, 4},
[2]int{5, 6},
}
fmt.Println(a11)
for _, v := range a11 {
fmt.Println(v)
for _, v2 := range v {
fmt.Println(v2)
}
}
map字典
map定义及常用方法
func main() {
// map是引用类型,必须初始化,默认是nil。map[key类型]value类型
// map[key的数据类型]value的数据类型
// 初始化1
m := make(map[int]string)
m[1] = "dog"
fmt.Println(m)
// 初始化2
m1 := map[string]string{
"1": "true",
"2": "false",
}
fmt.Println(m1)
// 初始化3(较为麻烦,需要两步)
var m2 map[string]string
m2 = map[string]string{}
m2["name"] = "lisi"
m2["age"] = "18"
fmt.Println(m2)
// 初始化4 (终极大招,使用interface可以接收任意value类型)
m3 := map[int]interface{}{}
m3[1] = 1
m3[2] = false
m3[3] = "str"
fmt.Println(m3)
// 删除map元素
delete(m3, 1)
// 查看元素在map中是否存在, map有两个返回值,第二个返回值,如果不存在key,那么ok为false,如果存在ok为true
captial, ok := m3[3]
if ok {
fmt.Println("exist is:", captial)
} else {
fmt.Println("not exist")
}
/*
map[1:dog]
map[1:true 2:false]
map[age:18 name:lisi]
map[1:1 2:false 3:str]
exist is: str
*/
}
map遍历
// 使用range遍历,如果是集合,返回 key 和 value
for key := range testMap {
fmt.Println(key, ":", testMap[key]) //打印 k,v
}
for k, v := range testMap {
fmt.Println(k, v)
}
指针
&符号取地址,*符号根据地址取值
func main() {
//指针--获取变量内存地址
a := "aaa"
fmt.Println("a 的内存地址: ", &a, "; a的原值: ", a)
//指针--通过内存找到原值
b := &a
//对内存地址赋值
*b = "bbb"
fmt.Println("b 的内存地址: ", b, "; b的原值: ", *b, "; a的新值: ", a)
//指针--空指针判断
var p *int
if p != nil {
fmt.Println("p is null")
} else if p == nil {
fmt.Println("p =", p)
}
}
func showMemAddr(i *int) {
fmt.Println(i) // 输出内存地址
fmt.Println(*i) // 输出原值
return
}
func main() {
i := 1
showMemAddr(&i)
fmt.Println(&i) // 输出内存地址
}
流程控制
运算符
比较运算符(操作的数据类型必须相同)
==
!=
>
<
<=
>=
算数运算符(加减乘除余)
+-*/%
逻辑运算符
&& 与:两个条件是否都为true
|| 或:两个条件至少一个为true
! 非:条件为false
if
if x := aaa; x > 10 {
fmt.Println(">10")
} else if x == 10 {
fmt.Println("=10")
} else {
fmt.Println("<10")
}
switch case
// 常规案例
i := 10
switch i {
case 1:
fmt.Println("i is equal to 1")
case 10, 11, 22:
fmt.Println("i is equal to 10 or 11 or 22")
default:
fmt.Println("All I know is that i is an integer")
}
// 使用运算符
age := 20
switch {
case age < 25:
fmt.Println("<25")
case age > 25:
fmt.Println(">25")
default:
fmt.Println("nono")
}
defer延时执行
1.在defer后指定的函数会在函数退出前调用,常用:资源清理、文件关闭、解锁、记录时间等
func ReadWrite() bool {
file.Open("file")
defer file.Close() // 只需调用一次,省事
if failureX {
return false
}
if failureY {
return false
}
return true
}
2.defer采用后进先出模式
for i:=0; i<5; i++ {
defer fmt.Printf("%d",i) // 43210
}
for循环
// 1. 常规for循环,有限制条件
count := 10
for i := 0; i < count; i++ {
fmt.Println(i)
}
// 2. 无限循环,等价于while True
for {
fmt.Println(1)
}
// 3. 对arr和map遍历使用for ... range
for i, n := range numbers {
fmt.Printf("index %d\n", i)
fmt.Printf("valuse %d\n", n)
}
goto跳转语句
func main() {
a := 0
A:
for a < 10 {
a++
fmt.Println(a)
if a == 10 {
break A
goto B
}
}
B:
fmt.Println("我是B")
}
/*
1
2
3
4
5
6
7
8
9
10
我是B
*/
函数
内置函数
append -- 用来追加元素到数组、slice中,返回修改后的数组、slice
close -- 主要用来关闭channel
delete -- 从map中删除key对应的value
panic -- 停止常规的goroutine (panic和recover:用来做错误处理)
recover -- 允许程序定义goroutine的panic动作
imag -- 返回complex的实部 (complex、real imag:用于创建和操作复数)
real -- 返回complex的虚部
make -- 用来分配内存,返回Type本身(只能应用于slice, map, channel)
new -- 用来分配内存,主要用来分配值类型,比如int、struct。返回指向Type的指针
cap -- capacity是容量的意思,用于返回某个类型的最大容量(只能用于切片和 map)
copy -- 用于复制和连接slice,返回复制的数目
len -- 来求长度,比如string、array、slice、map、channel ,返回长度
print、println -- 底层打印函数,在部署环境中建议使用 fmt 包
函数定义
func 函数名(参数)(返回值){
函数体
}
函数几种定义方法
package main
import "fmt"
// 1.有参数有返回值
func f0(x int, y int) (res int) {
// 法1
return x + y
// 法2
// res = x + y
// return // 可以省略返回值,当然也可以写上
}
// 2.无参数和返回值
func f1() {
fmt.Println("f1")
}
// 3.无返回值
func f2(x int, y int) {
fmt.Println(x + y)
}
// 4.无参数有返回值
func f3() int {
// 法1
// return 3
// 法2
res := 3
return res
}
// 5.1.多个返回值,用时变量数量需要与返回值数量一致,不需要可以用_占位,如:s41, s42, _ := f4()
func f4() (int, string, int) {
return 1, "dsd", 3
}
// 5.2.多个返回值
func f7(x int, y int) (sum int, sub int) {
sum = x + y
sub = x * y
// fmt.Println(sum, sub)
return
}
// 6.参数类型可简写
func f5(x, y, z int, m, n string, i, j bool) int {
return x + y
}
// 7.可变长参数... ,注意只能放到最后
func f6(x string, y ...int) {
fmt.Println(x)
fmt.Println(y) // y的类型是切片 [] int
}
// 8.函数作为参数
func add(x, y int) int {
return x + y
}
func sub(x, y int) int {
return x - y
}
func calc(x, y int, op func(int, int) int) int {
return op(x, y)
}
func main() {
s := f0(3, 5)
fmt.Println(s)
f1()
f2(1, 3)
s3 := f3()
fmt.Println(s3)
s41, s42, s43 := f4()
fmt.Println(s41, s42, s43)
s5 := f5(2, 2, 2, "m", "n", false, false)
fmt.Println(s5)
f6("呵呵呵", 1, 2, 3)
fff, ggg := f7(3, 3)
fmt.Println(fff, ggg)
ret2 := calc(10, 20, add)
fmt.Println(ret2)
}
init函数
程序启动自动调用,与python类似
匿名函数和闭包
匿名函数:常用于回调函数和闭包
// 9.闭包=函数+引用环境
func adder() func(int) int {
var x int
return func(y int) int {
x += y
return x
}
}
func main() {
// 匿名函数1.保存到变量
add := func(x, y int) {
fmt.Println(x + y)
}
add(3, 4)
// 匿名函数2.()直接执行
func(x, y int) {
fmt.Println(x, y)
}(20, 10)
// 闭包:变量bibao是一个函数并且它引用了其外部作用域中的x变量,此时在bibao的生命周期内,*变量x一直有效*。
var bibao = adder()
fmt.Println(bibao(10)) // 10
fmt.Println(bibao(20)) // 30
fmt.Println(bibao(30)) // 60
}
回调函数(函数作为值)
type typeName func(input1 inputType1 , input2 inputType2 [, …]) (result1 resultType1 [, …])
type Callback func(x,y int) int
// 说白了就是提供一个接口,通过外部函数实现具体方法
func test(x, y int, callback Callback) int {
return callback(x,y)
}
func add(x, y int) int {
return x + y
}
func del(x, y int) int {
return x - y
}
func main() {
x, y := 1,2
fmt.Println(test(x,y,add))
fmt.Println(test(x,y,del))
}
递归函数(函数自己调用自己)
// 递归函数,函数自己调用自己,超过一定的设定值return
func addMe(a, b int) int {
a = a + b
if a >= 10 {
fmt.Printf("over %v", a)
return a
}
fmt.Println("go on")
return addMe(a, b)
}
func main() {
addMe(1, 2)
}
struct 结构体类型
struct是一种数据类型,自己打造的用做一组 属性/字段 的容器。
方法与函数的区别:函数不属于任何类型,方法属于特定的结构体类型
func (接收者变量 接收者类型) 方法名(参数列表)(返回参数){
函数体
}
下面有写例子
//常用:定义struct
type person struct {
name string
age int
hobby []string
}
//继承/结构体嵌套
type student struct {
person
weight string
}
//自定义默认值
func Newperson(time string) person {
a := person{
Rating: 1, // fmt.Printf("%+v\n", Newperson()) 初始化不传参数输出零值,可以通过此方法设置默认值
}
return a
}
func main() {
// 初始化1
var p person
p.name = "zs"
p.age = 10
// 初始化2,若没有显示指定key,则需要把所有value都写完全
p1 := person{
"ls",
20,
[]string{"吃饭", "睡觉"},
}
// 初始化3
p2 := person{
name: "ww",
age: 30,
}
fmt.Println(p, p1, p2) // {zs 10 []} {ls 20 [吃饭 睡觉]} {ww 30 []}
// 初始化嵌套结构体
m := student{person{name:"zzd",age:18}, "80kg"}
// 访问字段
fmt.Println(m.name, m.age, m.weight)
// 修改字段
m.weight = "99kg"
fmt.Println(m.weight)
}
// 人结构体
type person struct {
name string
age int
}
// 地址结构体
type address struct {
province string
city string
person person // 结构体嵌套
}
/* 结构体嵌套使用
addr := address{
province: "bj",
city: "bj",
person: person{
name: "zzd",
age: 20,
},
}
fmt.Printf("%#v\n", addr) // main.address{province:"bj", city:"bj", person:main.person{name:"zzd", age:20}}
*/
// 构造函数(构造结构体变量的函数):用于处理结构体返回值(在多处使用时建议使用构造函数)
func newPerson(name string, age int) *person {
return &person{
name: name,
age: age,
}
}
/*
方法与函数的区别:函数不属于任何类型,方法属于特定的结构体类型
func (接收者变量 接收者类型) 方法名(参数列表)(返回参数){
函数体
}
*/
// person的方法Dream只有person可以调用
func (p person) Dream() {
fmt.Printf("%s有梦想\n", p.name)
}
// 指针接收者
// 1. 需要修改结构体变量值时使用
// 2. 结构体本身比较大,拷贝开销大时使用
// 3. 保持一致性:如果一个方法使用了指针接收者,其它方法也统一使用
func (p *person) SetAge(newAge int) {
p.age = newAge
}
// josn类型结构体,字段首字母需大写,可通过`json:name`打tag转小写,注意``内不能有空格
type people struct {
Name string `json:"name"`
Age int `json:"age`
}
// animal 结构体继承
type animal struct {
name string
}
// animal 实现了move函数
func (a *animal) move() {
fmt.Printf("%s会动\n", a.name)
}
// dog 结构体继承 animal
type dog struct {
feet int8
*animal // 通过嵌套匿名结构体实现继承
}
// dog 实现了 wang 函数
func (d *dog) wang() {
fmt.Printf("%s 汪汪汪\n", d.name)
}
func main() {
// 结构体初始化方法1:使用key value
p1 := person{
name: "huaz",
age: 18,
}
fmt.Println("p1:", p1) // p1: {huaz 18}
// 初始化方法2:赋值
var p2 person
p2.name = "zzd"
p2.age = 19
fmt.Println("p2:", p2) // p2: {zzd 19}
// 初始化方法3:使用值列表,顺序需一致
var p3 = person{
"dd",
29,
}
fmt.Println("p3:", p3) // p3: {dd 29}
// 指针kv初始化
p4 := &person{
name: "xiaozi",
age: 10,
}
fmt.Printf("p4: %#v\n", p4) // p4: &main.person{name:"xiaozi", age:10}
// 字段无初始值默认为零值
p5 := &person{
name: "haha",
}
fmt.Printf("p5: %#v\n", p5) // p5: &main.person{name:"haha", age:0}
// 空结构体不占空间
var v struct{}
fmt.Println(unsafe.Sizeof(v)) // 0
// 1.序列化:结构体变量 ---> json格式的字符串
// 2.反序列化:json格式 ---> 结构体变量
o1 := people{
Name: "dalin",
Age: 3,
}
// 序列化json
b, err := json.Marshal(o1)
if err != nil {
fmt.Printf("err: %v", err)
return
}
fmt.Printf("json: %v\n", string(b)) // json: {"name":"dalin","Age":3}
// 反序列化json
str := `{"name": "zz", "age": 10}`
var o2 people
json.Unmarshal([]byte(str), &o2)
fmt.Printf("ajson: %#v\n", o2) // ajson: main.people{Name:"zz", Age:10}
// 调用构造函数
p9 := newPerson("zs", 20)
fmt.Printf("%#v\n", p9) // &main.person{name:"zs", age:20}
// 调用方法
p9.Dream() // zs有梦想
p9.SetAge(30)
fmt.Println(p9.age)
// 初始化继承自animal的dog
d1 := &dog{
feet: 4,
animal: &animal{
name: "huahua",
},
}
fmt.Println(d1.name, d1.feet) // huahua 4
d1.move() // huahua会动
d1.wang() // huahua 汪汪汪
}
interface 接口类型
接口是方法的集合,是一种特殊的类型,它规定了变量有哪些方法。只要实现该接口的方法,都可以用这个接口接收。可以理解为定义了一种规范?必须实现接口定义的方法,才能使用。go提倡面向接口编程。
场景:不关心一个变量是什么类型,只关心能调用它什么方法(注:参数规则须一致)。如:多种数据库引擎的增删改查。
接口定义
type 名字 interface{
func1(arg1, arg2 ...)(return1, return2 ...)
func2(arg1, arg2 ...)(return1, return2 ...)
...
}
接口案例
package main
import "fmt"
// 接口嵌套示例
// type animal interface {
// mover
// eater
// }
// type mover interface {
// move()
// }
// type eater interface {
// eat(s string)
// }
// 定义一个car接口类型,只要实现了run()和color()都能被car调用,color()需要传一个参数
type car interface {
run()
color(s string)
}
// drive方法调用run()
func drive(c car) {
c.run()
}
// bmw 和 aodi都实现了run和color,就可以被car接口直接调用
type bmw struct {
name string
feet int8
}
// 使用值接收者实现接口和指针接收者实现接口的区别:
// 1.使用值接收者实现接口所有方法,结构体类型和结构体指针类型变量都能存
func (b bmw) run() {
fmt.Printf("%s速度70m\n", b.name)
}
func (b bmw) color(s string) {
fmt.Printf("%s色\n", s)
}
// 2.使用指针接收者实现接口所有方法,只能存结构体指针类型变量
// func (b *bmw) run() {
// fmt.Printf("%s速度70m\n", b.name)
// }
// func (b *bmw) color(s string) {
// fmt.Printf("%s色\n", s)
// }
type aodi struct {
name string
}
func (a aodi) run() {
fmt.Printf("%s速度80m\n", a.name)
}
func (a aodi) color(s string) {
fmt.Printf("%s色\n", s)
}
func main() {
var c1 car
fmt.Println(c1) // <nil>
// 使用值接收者调用
var b = bmw{
name: "宝马",
feet: 4,
}
// 使用指针接收者调用
// var b = &bmw{
// name: "宝马",
// feet: 4,
// }
var a = aodi{
name: "奥迪",
}
b.color("红") //红色
a.color("黄") //黄色
c1 = a
fmt.Println(c1) // {奥迪}
c1 = b
fmt.Println(c1) // {宝马}
drive(a) //奥迪速度80m
drive(b) //宝马速度70m
}
package包
1.首字母大写外部可见,小写只能在当前包内用,如
package pkg2
import "fmt"
// 包变量可见性
var a = 111 // 首字母小写,外部包不可见,只能在当前包内使用
// 首字母大写外部包可见,可在其他包中使用
const Mode = 1
type person struct { // 首字母小写,外部包不可见,只能在当前包内使用
name string
}
type Student struct {
Name string //可在包外访问的方法
class string //仅限包内访问的字段
}
type Payer interface {
init() //仅限包内访问的方法
Pay() //可在包外访问的方法
}
// 首字母大写,外部包可见,可在其他包中使用
func Add(x, y int) int {
return x + y
}
func age() { // 首字母小写,外部包不可见,只能在当前包内使用
var Age = 18 // 函数局部变量,外部包不可见,只能在当前函数内使用
fmt.Println(Age)
}
// init方法只要调用包就会调用,无论是否引用
func init() {
fmt.Println("this is init...")
}
2.包import导入的几种类型及ini()自动初始化调用
package main
import (
"fmt" // 标准倒入包
. "fmt" // 重命名包名写法1,可直接调用Println("haha"),不需要写fmt了,即将该文件下内容都导入进来
_ "os" // 导入匿名包,只导入包,包括init()方法,不导入包内普通方法。例如数据库连接包等
p2 "pkg2" // 重命名包名写法2
)
func main() {
a := p2.Mode // 可调用大写字母开头的变量
fmt.Println(a)
b := p2.Add(1, 4) // 可调用大写字母开头的方法
fmt.Println(b) // 标准包输出
Println("haha") // . "fmt" 包输出
}
=======输出=======
this is init...
1
5
haha
Goroutine并发编程
goroutine与线程
- 一个操作系统线程对应用户态多个goroutine。
- go程序可以同时使用多个操作系统线程。
- goroutine和OS线程是多对多的关系,即m:n。
可增长的栈内存
- OS线程(操作系统线程)一般都有固定的栈内存(通常为2MB),一个goroutine的栈在其生命周期开始时只有很小的栈(典型情况下2KB),goroutine的栈可以按需增大和缩小。
goroutine调度
- GPM是Go语言运行时(runtime)层面的实现,是go语言自己实现的一套调度系统。区别于操作系统调度OS线程。
- G就是goroutine,里面除了存放本goroutine信息外还有与所在P的绑定等信息。
- P管理着一组goroutine队列,P里面会存储当前goroutine运行的上下文环境(函数指针,堆栈地址及地址边界),P会对自己管理的goroutine队列做一些调度(比如把占用CPU时间较长的goroutine暂停、运行后续的goroutine等等)当自己的队列消费完了就去全局队列里取,如果全局队列里也消费完了会去其他P的队列里抢任务。
- M(machine)是Go运行时(runtime)对操作系统内核线程的虚拟, M与内核线程一般是一一映射的关系, 一个groutine最终是要放到M上执行。
通过sync.WaitGroup主动监听goroutine并发执行状态
// 通过sync.WaitGroup主动监听goroutine执行结束
var wg sync.WaitGroup
func wgTest(i int) {
time.Sleep(time.Second * time.Duration(rand.Intn(1)))
fmt.Println(i)
defer wg.Done() // wg执行完
}
func a() {
defer wg.Done() // wg执行完毕
for i := 1; i < 10; i++ {
fmt.Println("A:", i)
}
}
func b() {
defer wg.Done() // wg执行完毕
for i := 1; i < 10; i++ {
fmt.Println("B:", i)
}
}
func main() {
runtime.GOMAXPROCS(12) // 指定并发占用的cpu核数
for i := 0; i < 10; i++ {
wg.Add(1) // 启动一个goroutine wg计数器+1
go wgTest(i)
}
wg.Add(2) // wg计数器+2
go a()
go b()
wg.Wait() // wg等待上面流程执行
}
/*
并发结果输出通常都是乱序的
*/
通过channel实现Goroutine之间通信完成一组任务
- 如果说Goroutine是一种支持并发编程的方式,那么通道(chan)就是一种与Goroutine通信的方式。通道(chan)让数据能够进入和离开Goroutine,可方便Goroutine之间进行通信。channel是先进先出的。
不要通过共享内存来通信,而通过通信来共享内存
创建通道:c := make(chan (string))
向通道发送消息:c <- "sleep finish."
msg变量接收消息:msg := <-c
package main
import (
"errors"
"fmt"
"time"
)
// 3.slowfunc将通道作为参数
func slowfunc(c chan string) {
time.Sleep(time.Second * 2)
c <- "sleep finish." // 4.函数执行完毕向通道c发送一条消息
}
// 消息接收者遍历消息
func reciever(c chan string) {
for msg := range c {
fmt.Println(msg)
}
}
func pinger(c chan string) {
t := time.NewTicker(1 * time.Second) // 新建NewTicker定时器
// 向通道循环输入 ping
for {
c <- "ping"
<-t.C
}
}
// 指定通道访问权限
// 指定通道只读
func channelReader(messages <-chan string) {
msg := <-messages
fmt.Println(msg)
}
// 指定通道只写
func channelWriter(messages chan<- string) {
messages <- "Hello world"
}
// 指定通道读写
func channelReaderAndWriter(messages chan string) {
msg := <-messages
fmt.Println(msg)
}
// select验证通道case
func ping1(c chan string) {
time.Sleep(time.Second * 4)
c <- "ping1 finish"
}
func ping2(c chan string) {
time.Sleep(time.Second * 3)
c <- "ping2 finish"
}
// select验证无限阻塞
func sender(c chan string) {
t := time.NewTicker(1 * time.Second) // 定时器
for {
c <- "i am send a message" // 向通道c内发送消息
<-t.C
}
}
func main() {
// errors包创建错误
err := errors.New("some wrong")
if err != nil {
fmt.Println(err)
}
// fmt包格式化输出错误
name, role := "zhidao", "zhang"
errs := fmt.Errorf("the %v %v quit", role, name)
if err != nil {
fmt.Println(errs)
}
// 如果说Goroutine是一种支持并发编程的方式,那么通道就是一种与Goroutine通信的方式。通道让数据能够进入和离开Goroutine,可方便Goroutine之间进行通信。
// 不要通过共享内存来通信,而通过通信来共享内存
// 一、通道实例
c := make(chan (string)) // 1.创建通道
go slowfunc(c) // 2.go执行slowfunc函数,并将通道c传入
msg := <-c // 5.接收来自通道c的消息。阻塞直到收到消息为止,避免进程过早退出
fmt.Println(msg) // 6.接收成功打印消息
// 二、缓冲通道:没有接收者的情况下使用
message := make(chan (string), 2) // 1.创建长度为2的通道限制调度数量
message <- "hello"
message <- "world"
close(message) // 2.发送完消息关闭通道
fmt.Println("push two message onto chan")
time.Sleep(time.Second * 1)
reciever(message) // 3. 消息接收者
// 三、通道和流程控制
c1 := make(chan string) // 创建通道c1
go pinger(c1)
for i := 0; i < 2; i++ {
msg1 := <-c1 // 接收m1通道消息
fmt.Println(msg1)
}
// 四、使用select。select用于通道chan。跟switch用法很像,即:执行最先满足的条件
chan1 := make(chan string)
chan2 := make(chan string)
go ping1(chan1)
go ping2(chan2)
select {
case mm1 := <-chan1:
fmt.Println("recieved ", mm1)
case mm2 := <-chan2:
fmt.Println("recieved", mm2)
case <-time.After(500 * time.Millisecond): // 超时条件
fmt.Println("time out")
}
// 五、使用select无限阻塞,随时能够返回消息,最终设置超时退出通道判断
chan3 := make(chan string)
stop := make(chan bool) // bool的零值为false
go sender(chan3)
go func() {
time.Sleep(time.Second * 2)
fmt.Println("time is up!")
stop <- true // 向stop通道内发送true
}()
for {
select {
case <-stop: // 接收stop通道内的条件,为ture则执行此处逻辑
return
case msg := <-chan3: // 接收chan3通道的条件
fmt.Println(msg)
}
}
// ===========输出==============
// some wrong
// the zhang zhidao quit
// sleep finish.
// push two message onto chan
// hello
// world
// 例三:
// ping
// ping
// 例四:
// time out
// 例五:
// i am send a message
// i am send a message
// i am send a message
// time is up!
}
小技巧
处理错误errors和fmt.Errorf
func main() {
// errors包创建错误
err := errors.New("some wrong")
if err != nil {
fmt.Println(err)
}
// fmt包格式化输出错误
name, role := "zhidao", "zhang"
errs := fmt.Errorf("the %v %v quit", role, name)
if err != nil {
fmt.Println(errs)
}
}
比较两个字符串相等
fmt.Println("sa" == "sa") //true
比较两个slice相等(map、array类型)
dict := map[string]string{"A":"a","B":"b"}
dict1:=dict
fmt.Println(reflect.DeepEqual(dict,dict1)) //true