Copy暂略
Copy暂略
Copyfunc main() { // 变量定义的三种方法 // 1. var 变量名 类型 //只定义,不赋值 // var 变量名 类型 = 值 // 定义并赋初值 var a int = 3 fmt.Println(a) // 2. var 变量名 = 值 //类型推导 var b = 3 fmt.Println(b) // 3. 变量名 := 值 //推荐用法 c := 3 fmt.Println(c) // 交叉赋值 d := 1 e := 2 fmt.Println("交换前指针", &d, &e) d, e = e, d fmt.Println(d, e) fmt.Println(&d, &e)}
Copyfunc main() { // 常量的定义 使用 const 关键字 const a int = 3 const b = "qwerty" fmt.Println(a, b)}
Copy// int/uint int8/uint8 int16/uint16 int32/uint32 int64/uint64// float32/float64// byte/rune
Copy// stringfunc main() { var s1 string = "qwerty" s2 := "asdfgh" fmt.Println(s1) fmt.Println(s2)}
Copyfunc main() { //var a = [4]int{1, 2, 3, 4} // 一维数组 a := [4]int{1, 2, 3, 4} b := [3]string{"b1", "b2"} c := [4]int{2: 5} d := [...]int{1, 2, 3, 4} // 数组赋值 a[2] = 10 fmt.Println(a, b, c) // 数组遍历 for k, v := range a { fmt.Println(k, v) } // 多维数组 d := [2][3]int{{1, 2, 3}, {4, 5, 6}} fmt.Println(d)}
Copyfunc main() { // 切片的定义方式 // 1.申明切片 var s1[] int fmt.Println(s1) // 2, 使用 := s2 := []int{} fmt.Println(s2) // 3. 使用 make([]type, len, cap) var s3[] int = make([]int, 5) //容量为5 var s4[] int = make([]int, 3, 5) //大小为3,长度为5 fmt.Println(s3, len(s3), cap(s3)) fmt.Println(s4, len(s4), cap(s4)) // 初始化赋值 var s5 = []int{1, 2, 3} s6 := [] int{1, 2, 3} fmt.Println(s5,s6) // 从数组切片 arr := [5]int{1, 2, 3, 4, 5} // var s7 = arr[1:3] s7 := arr[1: 3] fmt.Println(s7) arr2 := [5]int{1, 2, 3, 4, 5} s8 := arr2[:4] fmt.Println(arr2, s8, len(s8), cap(s8)) s8 = append(s8, 5, 2) fmt.Println(s8, len(s8), cap(s8)) // 对比底层数组的起始指针 fmt.Println(&arr2[0], &s8[0]) // 切片遍历与数组一样}
Copymap 是 go 中 key-value 形式的一种数据结构,通过 key 可以获取到 value,类似于 Python 中的字典
Copy// key 的类型:必须是支持相等运算符(==,!=)的数据类型,如数字,字符串,指针,数组,结构体,以及对应的接口类型func main() { /* map 的定义 1. 使用 var 变量名 map[key 类型] value 类型 { key1: value1, key2: value2, ... } 2. 使用 make 创建 变量名 := make(map[key 类型] value 类型) 赋值操作,添加,删除等... */ m1 := map[string] int{ "a": 1, "b": 2, } fmt.Println(m1) m2 := make(map[string] int) m2["c"] = 3 m2["d"] = 4 fmt.Println(m2)}
Copyfunc main() { m = map[string] int{ "a": 1, "b": 2 } m["c"] = 3 //新增 m["a"] = 10 //修改 // 访问不存在的值,默认返回零值,但推荐使用 ok-idiom,因为返回零值无法判断元素是否存在 if v, ok := m["a"]; ok { // 判断 key 是否存在,返回 value fmt.Println(v) } delete(m, "a") // 删除元素,不存在不会报错}
Copyfunc main() { // 指针地址,指针类型,指针取值 // &取地址,*根据地址取值(解引用) a := 10 b := &a fmt.Println(*b) //指针类型 *int *unit *float *string *array *struct 等 // 指针的定义 // var 变量名 指针类型 var p1 *int //定义空指针 fmt.Println(p1) // nil p1 = &1 fmt.Println(p1)}
Copyfunc main() { arr := [4] int{1, 2, 3, 4} fmt.Println(arr) // 定义一个数组指针 var p1 *[4] int fmt.Println(p1) // nil 空指针 fmt.Ptintf("%T\n", p1) // *[4] int p1 = &arr1 fmt.Println(p1) fmt.Printf("%p\n", p1) fmt.Printf("%p\n", &p1) // 通过数组指针操作数组 (*p1)[0] = 100 // 可以简写为 // p1[0] = 200 fmt.Println(arr) // [100 2 3 4]}
Copyfunc main() { a := 1 b := 2 c := 3 d := 4 arr1 := [4] int{a, b, c, d} arr2 := [4] *int{&a, &b, &c, &d} fmt.Println(arr1) fmt.Println(arr2) // 操作数组与指针数组的区别 //arr1[0] = 100 //fmt.Println(a) //fmt.Println(arr1, &arr1[0]) // 值类型,将a,b,c,d的值拷贝过来放到数组中,修改的是数组空间内的数据,与a,b,c,d没有关系 //fmt.Println(arr2) *arr2[0] = 1000 fmt.Println(a) fmt.Println(arr1, &arr1[0]) fmt.Println(arr2, *arr2[0]) // 通过指针数组中的内存地址,修改了a,b,c,d的值,类似于Python列表 b = 2000 fmt.Println(b) fmt.Println(arr1, &arr1[1]) fmt.Println(arr2, *arr2[1])}
总结:
数组是值类型,将值拷贝了一份放到数内存中,二者相互独立,互不影响,修改数组后数组内存中的值改变,不会影响拷贝的源数据,源数据改变,也不会影响数组
Go中切片是对原数组的引用,二者互相关联,修改切片元素的值后,与之关联的底层数组的也会受到影响,同理,底层数组的改变也会影响切片的值
Python中的列表是引用类型,基于指针数组,修改可变元素后(可变类型与不可变类型),引用的源数据也会受到影响---
Copy// 函数指针// Go中函数默认就是一个指针类型,不需要*func main() { var a func() a = func1 a()}func func1() { fmt.Println("这是func1()")}// 指针函数// 返回值为指针的函数func main() { // arr1是数组,值传递,将func1中返回的arr的值拷贝到arr1中,当func1调用结束,arr被销毁 arr1 := func1() fmt.Printf("arr1的类型:%T,内存地址:%p,值:%v\n",arr1, &arr1, arr1) // arr2是指针类型,值传递,将func2中返回的arr的内存地址保存到arr2中,arr不会随着func2的结束而销毁(和闭包一样改变了变量的生命周期?) arr2 := func2() fmt.Printf("arr2的类型:%T,内存地址:%p,值:%v\n",arr2, &arr2, arr2)}// 这是普通函数func func1() { arr := [4] int{1, 2, 3, 4} return arr}// 这是指针函数func func2() *[4] int { arr := [4] int{1, 2, 3, 4} return &arr}
Copyfunc main() { /* 指针作为参数 参数:值传递和引用传递 总结:值传递,拷贝一份,不会影响原数据,但消耗内存 引用传递通过指针操作数据,会改变原数据,但节省内存(拷贝的数据可能很大) */ n := 10 // 值传递,func1改变不会影响n,a随着func1结束而被销毁 func1(n) fmt.Println(n) // 引用传递,将n的内存地址拷贝到a中,通过*a更改了n,a也会随着func2的结束而被销毁,但n已经改变 func2(&n) fmt.Println(n)}func func1(a int) { a = 100 fmt.Println(a)}func func1(a *int) { *a = 100 fmt.Println(*a)
见 7. 函数
见 9. 结构体
见 11. 接口
见 12.通道
Copy// true// false
Copyfunc main() { if 条件 { 代码块1 } else if 条件 { 代码块2 } else { 代码块3 }}
Copyfunc main() { // 完整结构 for 初始化;终止条件;自增{ 代码块 } // 在外部定义初始化值 i := 0 for ; i<10; i++ { 代码块 } // 在内部实现自增/自减 for i:=0; i<5; { i++ } // 利用for循环实现其他语言while的效果 for true { 代码块 }}
Copyfunc main() { // switch switch 变量名: { // 对此变量做条件判断 case 值1, 值2, ...: 代码块 case 值3, 值4, ...: 代码块 default: // 不管以上打码是否执行,此部分肯定执行 代码块 } }
Copy// break终止当前循环,执行循环下面的代码// continue终止本次循环,继续执行下次循环
Copy/*func 函数名(参数1 类型,参数2 类型,...)(返回值1 类型, 返回值2 类型,...){ //函数体}*/func f1(a int, b int)(c int){ return a+b}
Copy// 函数参数func f2(a, b int)() { //相邻的相同类型的参数可以合并 fmt.Println(a, b)}func f3(a int, b ...int) { // b ...int 表示接受int类型可变长参数,必须放在尾部 fmt.Println(a) fmt.Println(b...)}
Copy// 函数返回值func f1(a int, b int)(c int){ // 第二个括号中的返回参数必须与return的数量一致 return a+b}// 返回多个值,要有多个变量去接收,不想要的值可以用“_”忽略,不能将所有返回值都忽略_, a := f4(3, 4)func f4(a int, b int)(c int, d int){ reyurn a, b}
Copy// 匿名函数// 没有定义名字符号的函数// 可以复制给变量,作为参数,作为返回值使用func main() { func(s string) { fmt.Println(s) }("abc")}
外层函数中的局部变量被内层函数引用,外层函数的返回值是内层函数,按理来说,外层函数中的变量应该随着外层函数的结束而销毁,但由于它被内层函数引用,其生命周期也发生了改变
Copyfunc main() { res := closure() fmt.Printlm(res1()) // 1 fmt.Printlm(res1()) // 2}// 闭包函数func closure() func int { i := 0 return func() int { i++ return i }}
Copy// 延迟调用func main() { f,err := os.Open("./main.go") if err != nil { log.Fatalln(err) } drfer f.close() // 仅注册,直到main退出前才执行}// 多个延迟调用按栈(FILO)次序执行,最前面的最后执行
Golang没有类似try...catch 这种异常处理机制,而是使用 panic 和 recover 处理异常. 其实相当于python的raise。
Copy// error 是一种数据类型// 标准库将error定义为接口类型,以便实现自定义错误类型type error interface { Error() string}// 创建error对象var errDivByZore = errors.New("divis by zore")func div(x, y int) (int error) { if y == 0 { return 0, errDivByZore } return x/y, nil}func main() { z, err := div(5, 0) if err != nil { log.Fatalln(err) } fmt,Println(z)}
自定义错误类型
Copytype DivError struct { // 自定义错误类型 x, y int}func (DivError) Error() string { // 实现error接口方法 return "division by zore"}func div(x, y int) (int, error) { if y == 0 { return 0, DivError{x, y} // 返回自定义错误类型 } return x/y, nil}func main() { z, err := div(4, 0) if err != nil { switch e := err.(type) { // 根据类型匹配 case DivError: fmt.Println(e, e.x, e.y) default: fmt.Println(e) } log.Fatalln(err) } fmt.Println(z)}
更接近try/catch结构化异常,它们是内置函数而非语句
panic会立即中断当前函数流程,执行延迟调用(类似于Python中raise?)
在延迟调用函数中,recover可捕获并返回panic提交的错误对象
Copyfunc main() { defer func() { if err := recover(); err != nil { // 捕获错误 log.Fatalln(err) } }() panic("i am dead") // 引发错误 fmt.Println("exit.") // 永远不会执行}
总结:
使用panic抛出异常,抛出异常后将立即停止当前函数的执行并运行所有被defer的函数,然后将panic抛向上一层,直至程序crash。但是也可以使用被defer的recover函数来捕获异常阻止程序的崩溃,recover只有被defer后才是有意义的。
必须注意:
panic:主动抛出异常
recover :收集异常
recover 用来对panic的异常进行捕获. panic 用于向上传递异常,执行顺序是在 defer 之后。
defer有点类似try...catch...finally中的finally
Go语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念。Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。
Copy// 通过type关键字自定义类型type MyInt int //自定义了一个MyInt类型,它具有int类型的特性
Copytype TypeAlias = type //TypeAlias就是type的别名// 数据类型中,byte,rune也是别名,他们的定义如下type byte = uint8type rune = int32
Copy//类型定义type NewInt int//类型别名type MyInt = intfunc main() { var a NewInt var b MyInt fmt.Printf("type of a:%T\n", a) //type of a:main.NewInt fmt.Printf("type of b:%T\n", b) //type of b:int}// 总结:类型别名只是在代码中存在,编译后不会存在,而自定义类型会一直存在
Copy/*使用type和struct关键字来定义结构体,具体代码格式如下:type 类型名 struct { 字段名 字段类型 字段名 字段类型 ...}*/// 定义一个人类结构体type person struct { name string age int height int // age, height int // 同类型的字段可以写在一起}
Copy// var 结构体实例 结构体类型type person struct { name string age, height int}func main() { // 方法1 var p1 person p1.name = 'qqq' p1.age = 28 p1.height = 180 fmt.Ptintf("p1=%V\n", p1) fmt.Ptintf("p1=%#V\n", p1) // 方法2 p2 := person{"sss", 18, 149} fmt.Ptintf("p1=%V\n", p1) // 方法3 p3:= Person{} p3.name = 'yyy' p3.age = 18 p3.height = 149 fmt.Ptintf("p1=%V\n", p3) fmt.Ptintf("p1=%#V\n", p3) // 方法4 p4 := person{name="sss", age=18, height=149} fmt.Ptintf("p1=%V\n", p4)}
结构体是值传递,将一个实例赋值给另一个实例,发生的是深拷贝
Copyfunc main() { p1 := Person{ name: "qqq", age: 18, height: 180, } p2 := p1 fmt.Printf("%p\n", &p1) //二者内存地址不一样 fmt.Printf("%p\n", &p2) p2.name = "www" fmt.Println(p1) // {qqq 18 180} fmt.Println(p2) // {www 18 180} }type Person struct { name string, age int, height int,}
如果要创建引用类型的实例,那就要使用结构体指针实例化
Copyfunc main() { p1 := Person{ name: "qqq", age: 18, height: 180, } var p2 *Person p2 = p1 p2.name = "www" fmt.Println(p1) // {www 18 180} fmt.Println(p2) // &{www 18 180} }type Person struct { name string, age int, height int,}
Copy// 没有名字的结构体func main() { p1 := struct{ name string age int height int }{ name: "qqq", age: 18, height: 185, } fmt.Println(p1) // {qqq 18 158}}
Copyfunc main() { p1 := Person{"qqq", 18} fmt.Println(p1.string) fmt.Println(p1.int) }type Person struct { //name string string // 匿名字段,可通过实例.string获取 //age int int //匿名字段,可通过实例.int获取}/*总结: 结构体的匿名字段就是不写字段名,只写字段类型 可通过 实例.字段类型 来获取匿名字段的值 一个结构体中一种类型的匿名字段最多只能有一个*/
结构体内的字段类型为结构体,可通过 实例.嵌套结构体字段.字段 的方式访问和修改
结构体的嵌套类似于面向对象中的聚合
Copyfunc main() { addr := Address{ city: "上海市", zone: "浦东新区", } p := Person{ name: "qqq", age: 18, address: addr, } fmt.Println(p) // {qqq 18 {上海市 浦东新区}} fmt.Println(p.address.city) // 上海市 // 值传递,与嵌套的结构体互不影响 addr.zone = "杨浦区" p.address.city = "北京市" fmt.Println(p) // {qqq 18 {北京市 浦东新区}} fmt.Println(addr) // {上海市 杨浦区}}type Person struct{ name string age int address Address}type Address struct{ city string zone string}
Golang不是面向对象的语言,所以没有面向对象的三大特征,但我们可以用代码模拟出面向对象的部分特征
Copy// Go语言实现面向对象的继承func main() { // 实例化父类 p1 := Person{ name: "张三", age: 25, } fmt.Println(p1) fmt.Println(p1.name, p1.age) // 实例化子类 //e1 := Employee{Person{name:"李四", age:35}, "bolome"} e1 := Employee{ Person:Person{ name:"李四", age:35, }, company:"bolome", } fmt.Println(e1) //fmt.Println(e1.Employee.name, e1.Employee.age, e1.company) //提升字段,如果继承字段为匿名字段,可以省略嵌套的字段,直接点 fmt.Println(e1.name, e1.age, e1.company)}// 定义父类type Person struct{ name string age int}// 定义子类type Employee struct{ Person // 使用匿名字段,字段类型为结构体,模拟继承关系 company string // 子类新增属性}
模拟多继承
Copy// 模拟面向对象多继承func main() { c := C{ "ccc", 20, A{"aaa", 18}, B{"bbb", 28} } fmt.Println(c.name) // 这种写法要么C中有name属性,要么父类中只有一个有name属性,否则抛错:ambiguous selector c.name fmt.Println(c.A.name) // 如果要使用父类中重复的属性,必须声明是哪个父类 fmt.Println(c.B.name) fmt.Println(c.age1) fmt.Println(c.age2) fmt.Println(c.age)}// 定义父类Atype A struct { name string age1 string}// 定义父类Btype B struct { name string age2 int}// 定义子类Ctype C struct { name string age int A B}
方法是与对象实例绑定的特殊函数。这个对象可以是命名类型或者结构体的一个值或一个指针。
Copyfunc main() { p1 := Person{"qqq", 18} p1.talk() p1.run()}type Person struct{ name string age int}/*定义方法func (t type) methodName() { 代码块}*/func (p Person) talk() { fmt.Println("这是个talk方法")}func (p *Person) run() { //fmt.Println((*p).name) fmt.Println(p.name) fmt.Println("这是个run方法")}
Copy/* Go语言实现面向对象的继承 */// 定义父类type Person struct{ name string age int}// 定义子类type Employee struct{ Person // 使用匿名字段,字段类型为结构体,模拟继承关系 company string // 子类新增属性}// 定义父类方法func (p Person) talk() { fmt.Println("父类方法,talk")}// 子类新增方法func (e Empployee) run() { fmt.Println("子类新增方法,run")}//// 子类重写方法//func (e Employee) talk() {// fmt.Println("子类重写方法,talk")//}func main() { // 实例化父类 p1 := Person{ name: "张三", age: 25, } fmt.Println(p1) fmt.Println(p1.name, p1.age) // 实例化子类 //e1 := Employee{Person{name:"李四", age:35}, "bolome"} e1 := Employee{ Person:Person{ name:"李四", age:35, }, company:"bolome", } fmt.Println(e1) //fmt.Println(e1.Employee.name, e1.Employee.age, e1.company) //提升字段,如果继承字段为匿名字段,可以省略嵌套的字段,直接点 fmt.Println(e1.name, e1.age, e1.company) e1.talk() // 子类没有从写,使用父类,重写后使用子类的 e1.run() // 子类从写方法}
接口代表一种调用契约,是多个方法声明的集合。
接口要实现的是做什么,而不关心怎么做,谁来做。
Go中接口机制:只要目标方法集合包含接口声明的全部方法,就被视为实现了该接口,无需做显示声明(非入侵式)。
目标类型可实现多个接口。
接口通常以er作为后缀
Copytype tester interface { test() str() string}type data struct{}func (d *data) test() {}func (d data) str() string { return "" }func main() { var d data // var t tester = d // *data实现了接口中的两个方法,data只实现了一个,不能判断为实现了接口 var t tester = &d t.test() fmt.Println(t.str())}
Copy/* 一个比较好的接口实现案例 */// 定义一个USB接口type USB interface { startWork() // USB设备开始工作 stopWork() // USB设备结束工作}// 实现类type Mouse struct { name string}type KeyBoard struct { name string}// Mouse实现接口方法func (m Mouse) startWork() { fmt.Println("Mouse开始工作")}func (m Mouse) stopWork() { fmt.Println("Mouse结束工作")}// KeyBoard实现接口方法kfunc (k KeyBoard) startWork() { fmt.Println("KeyBoard开始工作")}func (k KeyBoard) stopWork() { fmt.Println("KeyBoard结束工作")}// 定义一个测试方法func testInterface(usb USB) { // 当需要接口类型对象时,可使用任意的实现类代替 usb.startWork() // 如果传入Mouse类型,usb = m,传入KeyBoard类型,usb = k usb.stopWork()}func main() { m := Mouse{"鼠标"} //m.startWork() //m.stopWork() testInterface(m) k := KeyBoard{"键盘"} //k.startWork() //k.stopWork() testInterface(k) var m1 USB m1 = m //m1.name // 接口对象不能访问实现类中的属性 m1.startWork() // 接口对象可以访问实现类中的方法 m1.stopWork()}
内部没有任何方法的接口,可以看做任何对象都实现了空接口
使用空接口可以向函数传入任意类型参数,定义可以接受任意类型参数的容器
Copytype A interface{}type Sct struct{ name string}func func1(a interface{}) { fmt.Println(a)}func main() { s := Sct{"aaa"} var a0 A = s // 空接口类型可以接受任意类型数值 var a1 A = 1 var a2 A = "aaa" var a3 A = func() {} var a4 A = 3.14 fmt.Println(a0) fmt.Println(a1) fmt.Println(a2) fmt.Println(a3) fmt.Println(a4) // 使用空接口向函数传入任意类型参数 func1(1) func1("abc") // 使用空接口定义可以接受任意类型参数的容器 m := map[int] interface{}{ 1: 1, 2: "qwerty", 3: [3] int{1, 2, 3}, } fmt.Println(m)}
接口嵌套类似面向对象中多继承的效果
Copyfunc main() { var s struct = struct{} var a1 A = s a1.test1() var b1 B = s b1.test2() var c1 C = s c1.test1() c1.test2() c1.test3()}type A interface{ test1()}type B interface{ test2()}type C interface{ A B test3()}type Sct struct {}func (s *Sct) test1() { fmt.Println("这是test1")}func (s *Sct) test2() { fmt.Println("这是test2")}func (s *Sct) test3() { // 如果要实现接口C,就要实现接口A和B fmt.Println("这是test3")}
因为Golang中所有类型都实现了空接口,要判断接口对象的实际类型,就要使用到断言
Copy//安全类型断言断言类型的值, 布尔值 := 表达式.(目标类型)//非安全类型断言断言类型的值 := 表达式.(目标类型)
Copytype A struct {}func main { a := A{} // t := a.(A) //非安全类型,如果断言失败,会直接panic t, ok := a.(A) //安全类型,如果断言失败,不会panic,ok为false if ok { fmt.Println(t) }}
Copytype A stringfunc main { var a A switch t := a.(type) { //这里优先匹配A case A: fmt.Println("A") case string: fmt.Println("string") default: fmt.Println("int") }}
channel是Go语言中各个并发结构体(goroutine)之前的通信机制。 通俗的讲,就是各个goroutine之间通信的”管道“,有点类似于Linux中的管道。
Copy// var 变量名 chan 数据类型// 方法一:channel的创建赋值var ch chan int;ch = make(chan int);// 方法二:短写法ch:=make(chan int);// 方法三:综合写法:全局写法!!!!var ch = make(chan int);
Copy//定义只读的channelread_only := make (<-chan int)//定义只写的channelwrite_only := make (chan<- int)
Copy//带缓冲区 (只有当队列塞满时发送者会阻塞,队列清空时接受着会阻塞。)ch := make(chan int, 10) /*无缓冲channel详细解释:1.一次只能传输一个数据2.同一时刻,同时有 读、写两端把持 channel,同步通信。如果只有读端,没有写端,那么 “读端”阻塞。如果只有写端,没有读端,那么 “写端”阻塞。读channel: <- channel写channel: channel <- 数据举一个形象的例子:同步通信: 数据发送端,和数据接收端,必须同时在线。 —— 无缓冲channel打电话。打电话只有等对方接收才会通,要不然只能阻塞*/
Copy//不带缓冲区ch := make(chan int)/*带缓channel详细解释:举一个形象的例子:异步通信:数据发送端,发送完数据,立即返回。数据接收端有可能立即读取,也可能延迟处理。 —— 有缓冲channel 不用等对方接受,只需发送过去就行。发信息。短信。发送完就好,管他什么时候读信息。*/
读写操作注意:
问题来了,如何知道channel是否关闭,如何优雅的关闭channel:
一个适用的原则是不要从接收端关闭channel,也不要关闭有多个并发发送者的channel。
关闭只读 channel 在语法上就彻底被禁止使用了。
Copy读取channel的方式有两种:close(ch)- 一种方式:value, ok := <- ch ok是false,就表示已经关闭。 - 另一种方式,就是上面例子中使用的方式: for value := range ch { } channel关闭之后,仍然可以从channel中读取剩余的数据,直到数据全部读取完成,会跳出循环
select是Golang在语言层面提供的多路IO复用的机制,其可以检测多个channel是否ready(即是否可读或可写)
Copypackage mainimport ( "fmt" "time")func main() { chan1 := make(chan int) chan2 := make(chan int) go func() { chan1 <- 1 time.Sleep(5 * time.Second) }() go func() { chan2 <- 1 time.Sleep(5 * time.Second) }() select { case <-chan1: fmt.Println("chan1 ready.") case <-chan2: fmt.Println("chan2 ready.") default: fmt.Println("default") } fmt.Println("main exit.")}
Goroutine是轻量级的线程,初始会在堆上分配4kb的内存
封装main函数的Goroutine称为主Goroutine,主Goroutine除了执行main函数,还进行了一系列其他操作:
设定每个Goroutine所能申请的栈空间最大尺寸,在32位计算机中,最大尺寸为250MB,在64位计算机中则为1GB。如果某个Goroutine的栈空间尺寸大于这个限制,那么运行时就会引发栈溢出(stack overflow)的恐慌,随后这个go程序会被终止。整个过程中主Goroutine会进行一系列的初始化工作,主要包含以下内容:
Copyfunc main() { go printNum() for i:=0;i<1000;i++ { fmt.Println("主Goroutine:",i) }}func printNum() { for i:=0;i<100;i++ { fmt.Println("子Goroutine":i) }}
在操作系统提供的内核线程之上,go搭建了一个特有的两级线程模型。Goroutine机制实现了M:N的线程模型,Goroutine机制是协程的一种实现,golang内置的调度器,可以让多核CPU中每个CPU执行一个协程。
Copy//go语言中新建一个goroutinego func() { //do something}()
我们分别用三角形,矩形和圆形表示Machine Processor和Goroutine。
如果两个M都在一个CPU上运行,这就是并发;如果两个M在不同的CPU上运行,这就是并行。在正常情况下,scheduler(调度器)会按照上面的流程进行调度,当一个G(Goroutine)的时间片结束后将P(Processor)分配给下一个G,但是线程会发生阻塞等情况,看一下goroutine对线程阻塞等的处理。
首先创建一个G对象,G对象保存到P本地队列或者是全局队列。P此时去唤醒一个M。P继续执行它的执行序。M寻找是否有空闲的P,如果有则将该G对象移动到它本身。接下来M执行一个调度循环(调用G对象->执行->清理线程→继续找新的Goroutine执行)。
M执行过程中,随时会发生上下文切换。当发生上线文切换时,需要对执行现场进行保护,以便下次被调度执行时进行现场恢复。Go调度器M的栈保存在G对象上,只需要将M所需要的寄存器(SP、PC等)保存到G对象上就可以实现现场保护。当这些寄存器数据被保护起来,就随时可以做上下文切换了,在中断之前把现场保存起来。如果此时G任务还没有执行完,M可以将任务重新丢到P的任务队列,等待下一次被调度执行。当再次被调度执行时,M通过访问G的vdsoSP、vdsoPC寄存器进行现场恢复(从上次中断位置继续执行)。
main函数所在的包,是Go程序的入口,main包想要引用别的代码,就需要使用inport导入
Copypackage xxx//大写开头表示可以导出func Abc() { //do something}
Copypackage mainimport xxx //可以使用绝对路径和相对路径//import a xxx //别名导入 a.Abcfunc main(){ xxx.Abc()}
Golang 的init函数和其他函数或方法有诸多不同. 它是 Golang package 初始化中使用的重要角色, 可以说是语法糖. 当对于 Golang 这样一门工程化编程语言来说,init函数有着很多巧妙的使用.
Tips:init函数并不一定需要写在源文件的最上面, 从语法层面说, 写在任何地方都可以. 标准库里有很多实践的例子.
Copy//待整理