Go是一门强类型静态编程语言
数据类型 GO语言提供了类型推导的语法糖,:=,注意,使用此声明变量的时候,左边的值中至少要有一个变量必须是为定义,否则会出现no new variables on left side of := ,而且它不能出现在全局的变量声明和初始化。
bool类型 布尔型的值只可以是常量 true 或者 false。一个简单的例子:var b bool = true
数值型 整数型 范围的计算机规则$$无符号的:0-2^n \ 有符号:-2^{n}/2,2^{n}/2 -1$$
int8 有符号 8 位整型 (-128 到 127) 长度:8bit
int16 有符号 16 位整型 (-32768 到 32767)
int32 有符号 32 位整型 (-2147483648 到 2147483647)
int64 有符号 64 位整型 (-9223372036854775808 到 9223372036854775807)
uint8 无符号 8 位整型 (0 到 255) 8位都用于表示数值:
uint16 无符号 16 位整型 (0 到 65535)
uint32 无符号 32 位整型 (0 到 4294967295)
uint64 无符号 64 位整型 (0 到 18446744073709551615)
浮点型
float32 32位浮点型数
float64 64位浮点型数
字符 Golang中没有专门的字符类型,如果要存储单个字符(字母),一般使用byte来保存。
字符串就是一串固定长度的字符连接起来的字符序列。Go的字符串是由单个字节连接起来的。也就是说对于传统的字符串是由字符组成的,而Go的字符串不同,它是由字节组成的 。 字符常量只能使用单引号('')括起来
go语言的字符有俩种类型
byte(代表ASCII编码的一个字符)
rune(代表UTF-8编码的一个字符)
字符串 字符串就是一串固定长度的字符连接起来的字符序列。Go 的字符串是由单个字节连接起来的。Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本。 用双引号("")括起来
注意
byte 等于 uint8
rune 等于 int32
多行字符串用``
统计字符长度
ASCII 字符串长度使用 len() 函数。
Unicode 字符串长度使用 utf8.RuneCountInString() 函数。
最好使用for range来遍历数组,切片,字符串,map,以及channel
code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package mainimport "fmt" func main () { var str = "abc北京" str2 := []rune (str) for i :=0 ; i < len (str); i++ { ch := str[i] fmt.Printf("str[%d]=%c\n" , i, ch) } for i, ch := range str { fmt.Printf("str[%d]=%c\n" , i, ch) } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package mainimport "fmt" func main () { var a int = 1 LOOP: for a < 10 { if a == 5 { a = a + 1 goto LOOP } if a == 3 { fmt.Printf("a的break值为 : %d\n" , a) break } fmt.Printf("a的值为 : %d\n" , a) a++ } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 package main import ( "fmt" "strings" ) func main () { var name string = "bobby:\"慕课网\"" fmt.Println(strings.Contains(name, "慕课网" )) fmt.Println(strings.Index(name, "慕课网" )) fmt.Println(strings.Count(name, "b" )) fmt.Println(strings.HasPrefix(name, "o" )) fmt.Println(strings.HasSuffix(name, "\"" )) fmt.Println(strings.ToUpper("bobby" )) fmt.Println(strings.ToLower("BOBBY" )) fmt.Println(strings.Compare("ab" , "aa" )) fmt.Println(strings.Compare("b" , "a" )) fmt.Println(strings.Compare("b" , "b" )) fmt.Println(strings.TrimSpace(" bobby " )) fmt.Println(strings.TrimLeft("bobby" , "b" )) fmt.Println(strings.Trim("bobby" , "b" )) fmt.Println(strings.Split("bobby imooc" , " " )) arrs := strings.Split("bobby imooc" , " " ) fmt.Println(strings.Join(arrs, "-" )) fmt.Println(strings.Replace("bobby: 18 电话:18888888" , "18" , "19" , 2 )) }
高级类型 数组 Go 语言提供了数组类型的数据结构。 数组是具有相同唯一类型的一组已编号且长度固定的数据项序列,这种类型可以是任意的原始类型例如整形、字符串或者自定义类型。 数组元素可以通过索引(位置)来读取(或者修改),索引从0开始,第一个元素索引为 0,第二个索引为 1,以此类推。数组的下标取值范围是从0开始,到长度减1。 数组一旦定义后,大小不能更改。
如果我就想在数组中定义不同类型,怎么办?
使用interface
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package mainimport "fmt" type container interface {}func main () { li2 := [...] container {1 , '1' , '一' , true } fmt.Println(li2) f := [...]int {0 :1 , 4 :1 , 9 :100 } fmt.Println(f) f2 := [2 ] string {"helo" , "world" , "nihao" } fmt.Println(f2) }
需要注意的是:数组是值类型 数组是值类型 Go中的数组是值类型,而不是引用类型。这意味着当它们被分配给一个新变量时,将把原始数组的副本分配给新变量。如果对新变量进行了更改,则不会在原始数组中反映。
注意:
数组的大小是类型的一部分。因此[5]int和[25]int是不同的类型。因此,数组不能被调整大小
切片 Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go中提供了一种灵活,功能强悍的内置类型切片(“动态数组”),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。 注意:
切片的传递是引用传递 直接声明var slice []int
newslice := *new([]int)
字面量slice := []int{1,2,3,4,5}
makeslice := make([]int, 5, 10)
从切片或数组“截取”slice := array[1:5] 或 slice := sourceSlice[1:5]
底层存储
https://blog.csdn.net/kikajack/article/details/79833674
go和python的切片区别
https://blog.csdn.net/qq_15437667/article/details/70191873
slice扩容机制
如果当前所需容量 (cap) 大于原先容量的两倍 (doublecap),则最终申请容量(newcap)为当前所需容量(cap);
如果<条件1>不满足,表示当前所需容量(cap)不大于原容量的两倍(doublecap),则进行如下判断;
如果原切片长度(old.len)小于1024,则最终申请容量(newcap)等于原容量的两倍(doublecap);
否则,最终申请容量(newcap,初始值等于 old.cap)每次增加 newcap/4,直到大于所需容量(cap)为止,然后,判断最终申请容量(newcap)是否溢出,如果溢出,最终申请容量(newcap)等于所需容量(cap);
map 在 Go 语言中,map 中的 key 类型必须是可比较的类型。这意味着 key 类型必须是能够进行相等性比较的类型,比如整数类型、字符串类型、指针类型等。 key的常用类型:int, rune, string, 结构体(每个元素需要支持 == or != 操作), 指针
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 { m1 := map [string ]string {"m1" : "v1" , } { m2 := make (map [string ]string ) m2["m2" ] = "v2" } { m3 := map [string ]string {} m4 := make (map [string ]string ) } { var m0 map [string ]string fmt.Println(m0) } { var m1 map [[]byte ]string fmt.Println(m1) 准确说slice类型只能与nil 比较,其他的都不可以,可以通过如下测试: var b1,b2 []byte fmt.Println(b1==b2) } { var m2 map [interface {}]string m2 = make (map [interface {}]string ) m2[[]byte ("k2" )]="v2" m2[123 ] = "123" m2[12.3 ] = "123" fmt.Println(m2) } { a3 := [3 ]int {1 , 2 , 3 } var m3 map [[3 ]int ]string m3 = make (map [[3 ]int ]string ) m3[a3] = "m3" fmt.Println(m3) } { type book1 struct { name string } var m4 map [book1]string fmt.Println(m4) } { type book2 struct { name string text []byte } var m5 map [book2]string fmt.Println(m5) }
注意:
遍历的顺序是随机的
使用for range遍历的时候,k,v使用的同一块内存,这也是容易出现错误的地方
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 m := map [string ]int { "a" : 1 , "b" : 2 , } for k, v := range m { fmt.Printf("k:[%v].v:[%v]\n" , k, v) } m := map [string ]int { "a" : 1 , "b" : 2 , } var bs []*int for k, v := range m { fmt.Printf("k:[%p].v:[%p]\n" , &k, &v) bs = append (bs, &v) } for _, b := range bs { fmt.Println(*b) }
make和new Go语言中 new 和 make 是两个内置函数,主要用来创建并分配类型的内存。 在声明变量的时候,都有默认值:
默认值是它所属类型的零值。
int 型它的零值为 0
string 的零值为 ""
引用类型的零值为 nil。
如果是一个引用类型,我们不仅要声明它,还要为它分配内存空间,否则我们赋值就无处安放 。
值类型的声明不需要我们分配内存空间,因为已经默认给我们分配好啦。
1 2 3 4 5 6 7 8 func new (Type) *Typevar p *int = new (int ) *p = 10 func make (t Type, size ...IntegerType) Typed := make ([]int , 0 )
区别:
new函数返回的是这个值的地址指针; make函数返回的是指定类型的实例
new 用于给类型分配内存空间,并且置零;make 只用于 chan,map,slice 的初始化。
接口( 接口命名习惯以 er 结尾) 在Go语言中接口(interface)是一种类型,一种抽象的类型。
interface是一组method的集合,是duck-type programming的一种体现。接口做的事情就像是定义一个协议(规则),只要一台机器有洗衣服和甩干的功能,我就称它为洗衣机。不关心属性(数据),只关心行为(方法)。
1 2 3 4 5 type 接口类型名 interface { 方法名1 ( 参数列表1 ) 返回值列表1 方法名2 ( 参数列表2 ) 返回值列表2 … }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 package mainimport ( "fmt" ) type Programmer interface { Coding() string Debug() string } type Designer interface { Design() string } type Manger interface { Programmer Designer Manage() string } type G struct {} type UIDesigner struct {} func (d UIDesigner) Design() string { fmt.Println("我会ui设计" ) return "我会ui设计" } type Pythoner struct { UIDesigner lib []string kj []string years int } func (p G) Coding() string { fmt.Println("go开发者" ) return "go开发者" } func (p G) Debug() string { fmt.Println("我会go的debug" ) return "我会go的debug" } func (p Pythoner) Coding() string { fmt.Println("python开发者" ) return "python开发者" } func (p Pythoner) Debug() string { fmt.Println("我会python的debug" ) return "我会python的debug" } func (p Pythoner) Manage() string { fmt.Println("不好意思,管理我也懂" ) return "不好意思,管理我也懂" } func main () { var pros []Programmer pros = append (pros, Pythoner{}) pros = append (pros, G{}) p := Pythoner{} fmt.Printf("%T\n" , p) var pro Programmer = Pythoner{} fmt.Printf("%T\n" , pro) var pro2 Programmer = G{} fmt.Printf("%T" , pro2) }
结构体(首字母大写公共,小写私有) Go 语言中的结构体(struct)是一种自定义的类型,它可以用来组合多个不同类型的数据字段(field)成为一个单独的实体。例如,你可以定义一个结构体表示人的信息,包括他的名字、年龄、身高等,而每个字段的类型可以是 string、int 或 float64 等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 package mainimport ( "fmt" "unsafe" ) type Course struct { Name string Price int Url string } func (c Course) printCourseInfo() { fmt.Printf("课程名:%s, 课程价格: %d, 课程的地址:%s" , c.Name, c.Price, c.Url) } func (c *Course) setPrice(price int ){ c.Price = price } func main () { var c Course = Course{ Name: "django" , Price: 100 , Url: "https://www.imooc.com" , } fmt.Println(c.Name, c.Price, c.Url) c2 := Course{"scrapy" , 110 , "https://www.imooc.com" } fmt.Println(c2.Name, c2.Price, c2.Url) c3 := &Course{"tornado" , 100 , "https://www.imooc.com" } fmt.Println(c3.Name, c3.Price, c3.Url) c4 := Course{} fmt.Println(c4.Price,) var c5 Course = Course{} var c6 Course var c7 *Course = &Course{} fmt.Println("零值初始化" ) fmt.Println(c5.Price) fmt.Println(c6.Price) fmt.Println(c7.Price) c8 := Course{"scrapy" , 110 , "https://www.imooc.com" } c9 := c8 fmt.Println(c8) fmt.Println(c9) c8.Price = 200 fmt.Println(c8) fmt.Println(c9) fmt.Println(unsafe.Sizeof(1 )) fmt.Println(unsafe.Sizeof("" )) fmt.Println(unsafe.Sizeof(c8)) type slice struct { array unsafe.Pointer len int cap int } s1 := []string {"django" , "tornado" , "scrapy" , "celery" , "snaic" , "flask" } fmt.Println("切片占用的内存:" , unsafe.Sizeof(s1)) m1 := map [string ]string { "bobby1" : "django" , "bobby2" : "tornado" , "bobby3" : "scrapy" , "bobby4" : "celery" , } fmt.Println(unsafe.Sizeof(m1)) c10 := Course{"scrapy" , 110 , "https://www.imooc.com" } (&c10).setPrice(200 ) fmt.Println(c10.Price) }
结构体标签 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 package mainimport ( "encoding/json" "fmt" "reflect" ) type Info struct { Name string `json:"name"` Age int `json:"age,omitempty"` Gender string `json:"-"` } func main () { info := Info{ Name: "bobby" , Gender:"男" , } re, _ := json.Marshal(info) fmt.Println(string (re)) t := reflect.TypeOf(info) fmt.Println("Type:" , t.Name()) fmt.Println("Kind:" , t.Kind()) for i := 0 ; i<t.NumField(); i++ { field := t.Field(i) tag := field.Tag.Get("orm" ) fmt.Printf("%d. %v (%v), tag: '%v'\n" , i+1 , field.Name, field.Type.Name(), tag) } }
结构体-方法 Go 语言不是面向对象的语言,它里面不存在类的概念,结构体正是类的替代品。类可以附加很多成员方法,结构体也可以。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package mainimport "fmt" import "math" type Circle struct { x int y int Radius int } func (c Circle) Area() float64 { return math.Pi * float64 (c.Radius) * float64 (c.Radius) } func (c Circle) Circumference() float64 { return 2 * math.Pi * float64 (c.Radius) } func main () { var c = Circle {Radius: 50 } fmt.Println(c.Area(), c.Circumference()) }
go语言中的type Go语言中的type关键字用于定义新的数据类型。在Go语言中,一个新的类型可以是一个结构体类型,接口类型,函数类型或基本类型(如int、string等)的别名。
例如,你可以使用type关键字定义一个新的类型MyInt,它是int类型的别名:
之后,你可以使用MyInt类型定义新的变量,就像使用其它基本类型一样:
作用域 golang中根据首字母的大小写来确定可以访问的权限。无论是方法名、常量、变量名还是结构体的名称,如果首字母大写,则可以被其他的包访问;如果首字母小写,则只能在本包中使用。 函数内部声明/定义的变量叫局部变量,作用域仅限于函数内部
1 2 3 4 5 6 7 func test () { age := 10 Name := "tom~" fmt.Println("age=" , age) fmt.Println("Name=" , Name) }
函数外部声明/定义的变量叫全局变量,作用域在整个包都有效,如果其首字母为大写,则作用域在整个程序有效
1 2 3 4 5 6 7 8 9 var age int = 50 var Name string = "jack~" func main () { fmt.Println("age=" , age) fmt.Println("Name=" , Name)
如果变量是在一个代码块,比如 for / if 中,那么这个变量的的作用域就在该代码块
1 2 3 4 5 6 7 8 9 10 11 func main () { for i := 0 ; i <= 10 ; i++ { fmt.Println("i=" , i) } var i int for i = 0 ; i <= 10 ; i++ { fmt.Println("i=" , i) } fmt.Println("i=" , i)
函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package mainimport ( "fmt" ) func xx () { fu := func () { fmt.Println(a) } fu() var a = 20 fmt.Println(a) } func main () { xx() }
函数声明包含一个函数名,参数列表, 返回值列表和函数体。如果函数没有返回值,则返回列表可以省略。函数从第一条语句开始执行,直到执行return语句或者执行函数的最后一条语句。
函数可以没有参数或接受多个参数。
注意类型在变量名之后 。
当两个或多个连续的函数命名参数是同一类型,则除了最后一个类型之外,其他都可以省略。
函数可以返回任意数量的返回值。
使用关键字 func 定义函数,左大括号依旧不能另起一行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package mainimport "fmt" func test (fn func () int ) int { return fn() } type FormatFunc func (s string , x, y int ) string func format (fn FormatFunc, s string , x, y int ) string { return fn(s, x, y) } func main () { s1 := test(func () int { return 100 }) s2 := format(func (s string , x, y int ) string { return fmt.Sprintf(s, x, y) }, "%d, %d" , 10 , 20 ) println (s1, s2) }
指针 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 package mainimport "fmt" func swap (a *int , b *int ) { c := *a *a = *b *b = c } func main () { a := 10 b := 20 fmt.Println(a, b) fmt.Printf("%p\n" , &a) var ip *int ip = &a *ip = 30 fmt.Println(a) fmt.Printf("ip所指向的内存空间地址是:%p, 内存中的值是: %d\n" ,ip, *ip) swap(&a, &b) fmt.Println(a, b) arr := [3 ]int {1 ,2 ,3 } var ip *[3 ]int = &arr var ptrs [3 ]*int if ip != nil { } }
defer,panic,error,recover defer特性 1. 关键字 defer 用于注册延迟调用。
2. 这些调用直到 return 前才被执。因此,可以用来做资源清理。
3. 多个defer语句,按先进后出的方式执行。
4. defer语句中的变量,在defer声明时就决定了。
5. defer之后只能是函数调用 不能是表达式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 package mainimport "fmt" func main () { x := 10 defer func (a *int ) { fmt.Println(x) fmt.Println(*a) }(&x) defer func () { x++ fmt.Println(x) }() x++ } x := 10 defer func () { x++ fmt.Println(x) }() x++ func a () int {x := 10 defer func (a int ) { fmt.Println(x) x++ }(x) tmp := x return tmp} fmt.Println(a())
panic 和 error 在go语言中,异常和错误是区分的。
错误是程序中可能出现的问题,比如连接数据库失败,连接网络失败等,在程序设计中,错误处理是业务的一部分。
而异常指的是不应该出现问题的地方出现了问题,比如空指针引用,下标越界,向空 map 添加键值等,这种情况在人们的意料之外
对于真正意外的情况,那些表示不可恢复的程序错误,不可恢复才使用 panic。对于其他的错误情况,我们应该是期望使用 error 来进行判定 Go 提供了两种创建error的方法,分别是:errors.New (https://pkg.go.dev/github.com/pkg/errors ) 如果有一个现成的 error ,我们需要对他进行再次包装处理,这时候有三个函数可以选择 // 新生成一个错误, 带堆栈信息 func New(message string) error
// 只附加新的信息 func WithMessage(err error, message string) error
// 只附加调用堆栈信息 func WithStack(err error) error
// 同时附加堆栈和信息 func Wrapf(err error, format string, args …interface{}) error
如果需要对源错误类型进行自定义判断可以使用 Cause,可以获得最根本的错误原因 // 获得最根本的错误原因 func Cause(err error) error
fmt.Errorf
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package mainimport ( "errors" "fmt" ) func main () { res1, err1 := div(1 , 1 ) fmt.Println(res1, err1) res2, err2 := div(1 , 0 ) fmt.Println(res2, err2) e := fmt.Errorf("自定义error" ) fmt.Println(e) } func div (n, m int ) (int , error ) { if m == 0 { return 0 , errors.New("0不能作为分母" ) } return m / n, nil }
一般在没有recover的情况下panic会导致程序崩溃,panic,defer和recover经常同时出现,用于异常处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package mainimport "fmt" func main () { defer doPanic() n := 0 res := 1 / n fmt.Println(res) } func doPanic () { err := recover () if err != nil { fmt.Println("捕获到panic" ) } }
Goroutine 进程,线程,协程:https://juejin.cn/post/7147756611929374756 https://zhuanlan.zhihu.com/p/94018082 A. 进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。 B. 线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。 C.一个进程可以创建和撤销多个线程;同一个进程中的多个线程之间可以并发执行。 协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。 线程:一个线程上可以跑多个协程,协程是轻量级的线程。
在java/c++中我们要实现并发编程的时候,我们通常需要自己维护一个线程池,并且需要自己去包装一个又一个的任务,同时需要自己去调度线程执行任务并维护上下文切换,这一切通常会耗费程序员大量的心智。那么能不能有一种机制,程序员只需要定义很多个任务,让系统去帮助我们把这些任务分配到CPU上实现并发执行呢?
Go语言中的goroutine就是这样一种机制,goroutine的概念类似于线程,但 goroutine是由Go的运行时(runtime)调度和管理的。Go程序会智能地将 goroutine 中的任务合理地分配给每个CPU。Go语言之所以被称为现代化的编程语言,就是因为它在语言层面已经内置了调度和上下文切换的机制。
Go语言中使用goroutine非常简单,只需要在调用函数的时候在前面加上go关键字,就可以为一个函数创建一个goroutine。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package mainimport ( "fmt" "sync" ) var wg sync.WaitGroupfunc f (n int ) { defer wg.Done() fmt.Println(n) } func main () { wg.Add(5 ) for i := 0 ; i<5 ; i++ { go f(i) } wg.Wait() }
go轻松开启100万个协程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package mainimport ( "fmt" "sync" "time" ) var wx sync.WaitGroupfunc main () { for i:=0 ; i < 1000000 ; i++ { wx.Add(1 ) go func (i int ) { defer wx.Done() for { fmt.Println(i) time.Sleep(time.Second*1 ) } }(i) } wx.Wait() } }
注意,主协程退出了,其他任务也会退出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 import ( "fmt" "time" ) func main () { go func () { i := 0 for { i++ fmt.Printf("new goroutine: i = %d\n" , i) time.Sleep(time.Second) } }() i := 0 for { i++ fmt.Printf("main goroutine: i = %d\n" , i) time.Sleep(time.Second) if i == 2 { break } } } package mainimport ( "fmt" "runtime" ) func main () { go func () { for i := 0 ; i < 5 ; i++ { fmt.Println("go" ) } }() for i := 0 ; i < 2 ; i++ { runtime.Gosched() fmt.Println("hello" ) } } package mainimport ( "fmt" "runtime" ) func main () { go func () { defer fmt.Println("A.defer" ) func () { defer fmt.Println("B.defer" ) runtime.Goexit() defer fmt.Println("C.defer" ) fmt.Println("B" ) }() fmt.Println("A" ) }() for { } }
总结: 单从线程调度讲,Go语言相比起其他语言的优势在于OS线程是由OS内核来调度的,goroutine则是由Go运行时(runtime)自己的调度器调度的,这个调度器使用一个称为m:n调度的技术(复用/调度m个goroutine到n个OS线程)。 其一大特点是goroutine的调度是在用户态下完成的, 不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池, 不直接调用系统的malloc函数(除非内存池需要改变),成本比调度OS线程低很多。 另一方面充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上, 再加上本身goroutine的超轻量,以上种种保证了go调度方面的性能。
Channel 虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发生竞态问题。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。
Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 var 变量 chan 元素类型声明 var msg chan int 初始化 msg = make (chan int ) msg = make (chan int , 16 ) 将一个值发送到通道中 ch <- 10 从一个通道中接收值 x := <- ch <-ch close 函数来关闭通道close (ch)v, ok := <-ch 单向通道 1. chan <- int 是一个只能发送的通道,可以发送但是不能接收;2. <-chan int 是一个只能接收的通道,可以接收但是不能发送。关于关闭通道需要注意的事情是,只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。 关闭后的通道有以下特点: 1. 对一个关闭的通道再发送值就会导致panic 。2. 对一个关闭的通道进行接收会一直获取值直到通道为空。3. 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。4. 关闭一个已经关闭的通道会导致panic 。func recv (c chan int ) { ret := <-c fmt.Println("接收成功" , ret) } func main () { ch := make (chan int ) go recv(ch) ch <- 10 fmt.Println("发送成功" ) }
select select语句选择一组可能的send操作和receive操作去处理。它类似switch,但是只是用来处理通讯(communication)操作。
如果有同时多个case去处理,比如同时有多个channel可以接收数据,那么Go会伪随机的选择一个case处理(pseudo-random)。如果没有case需要处理,则会选择default去处理,如果default case存在的情况下。如果没有default case,则select语句会阻塞,直到某个case需要处理。
需要注意的是,nil channel上的操作会一直被阻塞,如果没有default case,只有nil channel的select会一直被阻塞。
timeout select有很重要的一个应用就是超时处理。 因为上面我们提到,如果没有case需要处理,select语句就会一直阻塞着。这时候我们可能就需要一个超时操作,用来处理超时的情况。
下面这个例子我们会在2秒后往channel c1中发送一个数据,但是select设置为1秒超时,因此我们会打印出timeout 1,而不是result 1。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import "time" import "fmt" func main () { c1 := make (chan string , 1 ) go func () { time.Sleep(time.Second * 2 ) c1 <- "result 1" }() select { case res := <-c1: fmt.Println(res) case <-time.After(time.Second * 1 ): fmt.Println("timeout 1" ) } }
其实它利用的是time.After方法,它返回一个类型为<-chan Time的单向的channel,在指定的时间发送一个当前时间给返回的channel中。
Timer和Ticker timer是一个定时器,代表未来的一个单一事件,你可以告诉timer你要等待多长时间,它提供一个Channel,在将来的那个时间那个Channel提供了一个时间值。下面的例子中第二行会阻塞2秒钟左右的时间,直到时间到了才会继续执行。
当然如果你只是想单纯的等待的话,可以使用time.Sleep来实现。
你还可以使用timer.Stop来停止计时器。
ticker是一个定时触发的计时器,它会以一个间隔(interval)往Channel发送一个事件(当前时间),而Channel的接收者可以以固定的时间间隔从Channel中读取事件。下面的例子中ticker每500毫秒触发一次,你可以观察输出的时间。
类似timer, ticker也可以通过Stop方法来停止。一旦它停止,接收者不再会从channel中接收数据了。