@dungan
2022-11-29T09:26:54.000000Z
字数 43676
阅读 552
Go
包导入
import (// 导入内置包"fmt""log""net/http""time"// 导入当前项目下的包"http/sessionManager""http/sessionProvider"//匿名导入_ "packageA" //导入不使用//内嵌导入. "packageB" //类似php的trait,内嵌一个包的方法到当前包中//别名导入"crypto/rand"mrand "math/rand" // 将名称替换为mrand避免冲突)
空白标识符 _
_ 用于忽略某个变量或返回值,或者只是执行导入包的init()函数; Go 语言要求所有变量或导入的包都必须被使用,如果你不想使用,这时候就可以使用空白标识符 _ 来忽略。
init 函数
一个包中可以有个名为 init 的函数来负责包的初始化, 相当于构造方法construct,它没有返回值,init 函数会在 main 函数之前执行, 如果导入了其他包,则先执行其他包的init函数,注意:如果同一个包内的多个 init 顺序是不受保证的。
func init() {fmt.Println("execute before than main")}
bool、string、complex64、complex128、arrayint, uint,int8、uint8(byte)、int16、uint16、int32(rune)、uint32、int64、uint64float32、float64
slicemapchan
使用 type 关键字,基于一个现有的类型创造新的类型称之为自定义类型 或者 类型定义。
type Fuck int64type T structType I interface
关键字
break default func interface selectcase defer go map structchan else goto package switchconst fallthrough if range typecontinue for import return var
函数
append 用来追加元素到数组、slice中,返回修改后的数组、sliceclose 主要用来关闭channeldelete 从map中删除key对应的valuepanic 停止常规的goroutine (panic和recover:用来做错误处理)recover 允许程序定义goroutine的panic动作real 返回complex的实部 (complex、real imag:用于创建和操作复数)imag 返回complex的虚部make 用来分配内存,返回Type本身(只能应用于slice, map, channel)new 用来分配内存,主要用来分配值类型,比如int、struct。返回指向Type的指针cap capacity是容量的意思,用于返回某个类型的最大容量(只能用于切片和 map)copy 用于复制和连接slice,返回复制的数目len 获取长度,比如string、array、slice、map、channel ,返回长度print、println 底层打印函数,在部署环境中建议使用 fmt 包
全局变量与局部变量
- 全局变量:在函数体外声明的变量称之为全局变量,全局变量可以在整个包甚至外部包(被导出后)使用。
- 局部变量:在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,参数和返回值变量也是局部变量
大多数变量的默认值是其类型的零值。nil 是 interface、function、pointer、map、slice 和 channel 类型变量的默认初始值。
一、一行声明一个变量
var name stingvar title sting = "Python编程时光"
二、多个变量一起声明
var (name stringage intgender string)
三、短变量申明(函数特有)
name := "Python编程时光"
四、声明和初始化多个变量
name, age := "wangbm", 28
常量使用 const 关键字申明,注意:常量不能用 := 语法声明。
const (A = "A"B = 1)const C = true
iota是常量的计数器,从0开始,组中每定义1个常量自动递增1,每遇到一个const关键字,iota就会重置为0。
package mainimport ("fmt")func main() {const (A = "A"BC = iotaD)const E = iotafmt.Println(A, B, C, D, E) // A A 2 3 0}
类型转换时一般需要显式的使用某个类型的表达式进行转换,例如表达式 T(v) 将值 v 转换为类型 T。
package mainimport ("fmt""reflect""strconv")func main() {//同类型转换,只能在两种相互兼容的类型之间f := 3.15int_f := int(f) //浮点变整形fmt.Println(int_f)bool := truefmt.Println(int(bool)) //报错,整型无法和bool类型兼容//int 和 string 转换num := 3str := strconv.Itoa(num) // 字符串 3num, _ = strconv.Atoi(str) // int 3fmt.Println(reflect.TypeOf(str), str, num)//ascii 转换zs := 65asc := string(zs)fmt.Println(asc) // A//浮点型精度处理a := 1690 // 表示1.69b := 1700 // 表示1.70c := a * b // 结果应该是2873000表示 2.873fmt.Println(c) // 2873000fmt.Println(float64(c) / 1000000) // 2.873}
不仅可以使用内置类型进行转换,我们自定义的类型也可以进行转换
package mainimport ("fmt")type tcl intfunc main() {num := 456p := (*tcl)(&num)val, isTcl := interface{}(p).(*tcl)fmt.Println(*val, isTcl) // 456, true}
import "fmt"func main() {// 单个字符的类型可以申明为 bytevar a byte = 'A'var b uint8 = 'B'fmt.Printf("a 的值: %c \nb 的值: %c", a, b)}
由于单个 byte 代表一个ACSII字符,那多个 byte 放在一起就组成了字符串,也就是 string 类型(一堆byte组成了字符串,即 byte[]);使用 byte[]来进行默认字符串处理 ,性能和扩展性都有照顾。
func main() {// string 转 byte数组byteAB := []byte("AB") // [97 98]// byte数组 转 stringstringAB := string(byteAB) // ABfmt.Println(byteAB, stringAB)}
要修改字符串,需要先将其转换成[]rune或[]byte,完成后再转换为string。因为一个 UTF8 编码的字符可能会占多个字节,因此更推荐使用 []rune 来处理字符。
s1 := "hello"//转换为字节数组byteS1 := []byte(s1)byteS1[0] = 'H'fmt.Println(string(byteS1))s2 := "博客"//转换为字节数组runeS2 := []rune(s2)runeS2[0] = '狗'fmt.Println(string(runeS2))
go 函数可以接受多个参数,并且在函数名之后可以指出返回的类型。
package mainimport "fmt"func add(x int, y int) int {return x + y}func main() {fmt.Println(add(42, 13))}
如果参数数量不确定,可以通过 ...type 来定义不定参数函数。
... type只能是位于函数参数最后,如果你希望函数的参数传任意类型,可以指定类型为 interface{}。
package mainimport "fmt"func myFunc(args ...int) {// 使用for循环获取每个输入的参数for _, arg := range args {fmt.Println(arg)}//在内部转发参数myFunc2(args ...)}func myFunc2(args ...int){fmt.Println(args)}func main() {myFunc(2, 3, 4)}
如果函数体中的变量名和返回值的变量名一样,你无需显式的 return 这个变量。
func (manager *Manager) SessionStart() (session Session) {...//生成 sessionsession, _ = manager.provider.SessionInit(sid)...return}
函数还可以有多个返回值。
package mainimport "fmt"func swap(x, y string) (string, string) {return y, x}func main() {a, b := swap("hello", "world")fmt.Println(a, b)}
nil 是任意类型的0值,当一个函数你没有要求可返回的类型,那么你就返回 nil。
func (provider MemoryDriver) SessionRead(sid string) (sessionManager.Session, error) {...return nil, nil}
函数可以用作另一函数的参数,也就是所谓的回调函数。
package mainimport ("fmt""math")func compute(x,y float64,fn func(float64, float64) float64) float64 {return fn(x, y)}func main() {hypot := func(x, y float64) float64 {return math.Sqrt(x*x + y*y)}fmt.Println(compute(3, 4, hypot))}
函数的返回值类型如果是函数,这时候就形成了闭包。
package mainimport ("fmt")//将 【func(int) int】 看作一个整体,意为 a() 函数的返回值类型为函数func a() (func(n int) int) {// 这里的func 没有函数名,我们称之为匿名函数return func(n int) int {return n+1}}func main() {b := a()fmt.Println(b(5)) // 6}
对于大多数函数,如果要返回错误,将 error 作为多个返回值中的最后一个。
func ListPosts(...) ([]Post, error) {conn, err := gorm.Open(...)if err != nil {return []Post{}, err}var posts []Postif err := conn.Find(&posts).Error; err != nil {return []Post{}, err}return posts, nil}
当然,我们还可以生成自定义 error 信息。
var ErrTimeOut = errors.New("执行者执行超时")var ErrInterrupt = errors.New("执行者被中断")var fmtError = fmt.Errorf("借助fmt生成error信息")//函数调用判断是否为error...err := r.run()if(err != nil){// 出错了return fmtError}
defer 会在当前函数返回前执行传入的函数,它会经常被用于关闭文件描述符、关闭数据库连接以及解锁资源
defer 函数会按照后进先出的顺序调用(最早定义的最后执行)。
package mainimport "fmt"func main() {fmt.Println("counting") // 立即输出// 最先输出的是9,符合最早定义,最后执行的特点for i := 0; i < 10; i++ {defer fmt.Println(i)}fmt.Println("done") // 立即输出}
外层函数中的 return 语句会等到 defer 函数执行完毕后才会返回。
package mainimport "fmt"func main() {fmt.Println(testDefer()) // 2}func testDefer() (n int){i := 1// 这里通过匿名的自执行函数来求值defer func(i int) {n = i + 1}(i)return n}
在 panic 抛出异常之前,会先处理完当前协程上已经defer 的任务,执行完成后再抛出。
func main() {defer func() {fmt.Println("defer func")}()arr := []int{1, 2, 3}fmt.Println(arr[4])}
这两个函数是go的异常处理函数,panic() 相当于抛出异常,recover() 相当于捕获异常。
- panic : 用来抛出错误,该函数接收任意类型的数据,比如整型,字符串,对象等,panic 退出前会执行 defer 指定的内容。
- recover : 用来捕获由 panic 函数抛出的错误,recover 函数应放置于函数体的最前面,并且只有在 defer 调用的函数中有效。
package mainimport ("errors""fmt")func main() {err := doSomeThing()if err != nil {fmt.Println(err)} else {fmt.Println("success")}}func doSomeThing() (err error) {defer func() {if r := recover(); r != nil {//通过类型断言,将捕获的错误值转换为 string 类型,并最终将捕获的错误转换成 error类型返回给调用者str , ok := r.(string)if ok {err = errors.New(str)} else {panic("new error")}}}()panic("some error")return err}
go 只有 for 这一种循环结构,for 是 Go 中的 "while"。需要注意 for,if,switch 都具有块级作用域,即在语句外部无法在语句中定义的变量。
package mainimport "fmt"func main() {// for循环sum := 0for i := 0; i < 10; i++ {sum += i}fmt.Println(sum)fmt.Println(i) //报错 undefined: i// while 循环sum := 1for sum < 1000 {sum += sum}fmt.Println(sum)}
可以使用 for range 对数组,切片,map,通道等迭代。
package mainimport ("fmt")func main() {slice := []int{1, 2, 3, 4, 5}// 普通遍历for i, v := range slice {fmt.Println(i , v)}// 如果不想要索引,可以使用_来忽略它for _, v := range slice {fmt.Println(v)}// 如果你用一个变量来接收的话,接收到的是索引for i := range slice {fmt.Println(i)}//range也可以用在map的键值对上。kvs := map[string]string{"a": "apple", "b": "banana"}for k, v := range kvs {fmt.Printf("%s -> %s\n", k, v)}//range还可以用来枚举字符串。for i2, c := range "go" {fmt.Println(i2, string(c)) //得出的c是ASCII值}}
if 语句与 for 循环类似,表达式外无需小括号 ( ) ,而大括号 { } 则是必须的。
func main() {a := 10if a := 1; a > 0 {a++fmt.Println(a) // 2} else if a < 0 {a--fmt.Println(a)}fmt.Println(a) // 10,说明if的a只是在局部有用}
不同于其他语言中的switch语句,go 中一旦条件符合则会自动终止,不需要 break,而如果你希望继续执行下一个case,需使用 fallthrough 语句。
G另一点重要的不同在于 switch 的 case 无需为常量,且取值不必为整数。
package mainimport ("fmt""runtime""time")func main() {fmt.Print("Go runs on ")os := runtime.GOOSswitch os {case "darwin":fmt.Println("OS X.")case "linux":fmt.Println("Linux.")default:fmt.Printf("%s.\n", os)}// fallthrough 语句switch a := 200; {case a == 100:fmt.Println("a=100")case a == 200:fmt.Println("a=200")fallthrough // 不退出,进入下个case语句default:fmt.Println("a=" + strconv.Itoa(a+a))}// 没有没有条件的 switch 能将一长串 if-then-else 写得更加清晰t := time.Now()switch {case t.Hour() < 12:fmt.Println("Good morning!")case t.Hour() < 17:fmt.Println("Good afternoon.")default:fmt.Println("Good evening.")}}
Go中支持空block{},这个大括号有自己的作用域,里面的代码只执行一次,退出大括号就退出作用域。
func main() {{v := 1{v := 2fmt.Println(v) // 输出2}fmt.Println(v) // 输出1}}
Go语言中的指针操作非常简单,只需要记住两个符号:&(取地址) 和 *(根据地址取值),取地址操作符&和取值操作符*是一对互补操作符,&取出地址,*根据地址取出地址指向的值。
package mainimport ("fmt")func main() {//================= 指向变量的指针 =====================var num = 3var p *int = &num // 指针类型的变量,值必须是用 & 标识的内存地址var np *int = 3 // 报错, cannot use 3 (type int) as type *int in assignmentfmt.Println(&num) //0xc0420080c8fmt.Println(p); //0xc0420080c8,p和num指向了同一个内存地址fmt.Println(*p) //3//=============== 指向指针的指针 ======================var pp **intpp = &pfmt.Println(pp); //0xc042004028,获取指针 p 的内存地址,而指针 p 的值存的是它指向的内存地址fmt.Println(*pp); //0xc0420080c8,获取指针p 指向的内存地址fmt.Println(**pp) //3 ,** 获取指针p 指向的内存地址的值}
类型 [n]T 表示拥有 n 个 T 类型的值的数组,数组用作函数参数是值传递。
package mainimport "fmt"func main() {var a [2]stringa[0] = "Hello"a[1] = "World"fmt.Println(a[0], a[1])fmt.Println(a)// 注意:{} 中的元素个数不能大于 [] 中的数字primes := [6]int{2, 3, 5, 7, 11, 13}fmt.Println(primes)}
切片是对数组一个连续片段的引用,没有设置元素个数的数组我们称之为切片, []T 表示一个元素类型为 T 的切片,切片用作函数参数是引用传递,切片比数组更常用,Go 中的大部分数组编程都是通过切片来完成的。
切片的零值是 nil,nil 切片的长度和容量为 0 且没有底层数组
package mainimport "fmt"func main() {var s []intfmt.Println(s, len(s), cap(s))if s == nil {fmt.Println("nil!")}}
冒号分隔后的切片或数组,包括第一个元素,但排除最后一个元素。
package mainimport "fmt"func main() {primes := [6]int{2, 3, 5, 7, 11, 13}var s []int = primes[1:4]fmt.Println(s) // [3 5 7]}
切片是对数组的引用,切片并不存储任何数据,它只是描述了底层数组中的一段,更改切片的元素会修改其底层数组中对应的元素。
package mainimport "fmt"func main() {names := [4]string{"John","Paul","George","Ringo",}fmt.Println(names) // [John Paul George Ringo]b := names[1:3]b[0] = "XXX"fmt.Println(names) // [John XXX George Ringo]}
相关函数
- len() : 获取切片的元素个数。
- cap() : 获取切片的容量。
- make([]T, len, cap) : 创建切片,len 是数组的元素个数,cap 是数组的容量,如果cap 省略,则其值和 len 相同。
- copy(dest, from) :复制切片,会在底层创建一个匿名的数组,这样就切断了与原切片间的引用关系,实现的效果是会覆盖相同索引处的元素,如果没有对应的索引,dest不变。
- append() : 向数组/切片追加元素。
切片截取
package mainimport "fmt"func main() {a := make([]int, 5)printSlice("a", a)b := make([]int, 0, 5)printSlice("b", b)c := b[:2]printSlice("c", c)d := c[2:5]printSlice("d", d)}func printSlice(s string, x []int) {fmt.Printf("%s len=%d cap=%d %v\n",s, len(x), cap(x), x)}
使用 append 追加元素
package mainimport ("fmt")func main() {s := []int {}s2 := []int {1,2,3}s = append(s, 4,5,6) // 添加元素到切片s2= append(s2, s...) // 通过...操作符合并切片fmt.Println(s, s2) // [4 5 6] [1 2 3 4 5 6]}
使用 copy 复制切片
package mainimport ("fmt")func main() {s := []int{1}arr := []int{4, 5, 6, 7, 8, 9}copy(arr, s)fmt.Println(arr, s) //[1 5 6 7 8 9] [1],arr和s有相同的索引(index=0),所以index=0处的4被覆盖成了1s[0] = 0fmt.Println(arr, s) //[1 5 6 7 8 9] [0], 切断了引用关系,即使修改了源切片也不会影响copy生成的切片//复制整个切片arr2 := []int{1, 2, 3, 4, 5}arr3 := make([]int, len(arr2))copy(arr3, arr2)arr3[0] = 0fmt.Println(arr2, arr3) // [1 2 3 4 5] [0 2 3 4 5]}
字符串的底层就是一个 byte 的数组,因此,也可以进行切片操作
package mainimport ("fmt""strings")func main() {host := "www.baidu.com"pdIndex := strings.LastIndex(host, ".")fmt.Println(host[pdIndex:]) //.coms := []byte(host)s[0] = 'W'fmt.Println(string(s)) //Www.baidu.com}
map 其实就是key-value形式的数据结构,在别的语言也叫hash表,字典或关联数组。
申明但未初始化的 Map 零值是 nil,既没有键,也不能添加键。
package mainimport ("fmt")func main(){var m1 map[string]stringfmt.Println(m1, m1 == nil) // map[] true}
可以在申明时给 Map 赋值或者通过 make 函数来初始化 Map,当然可以用任意类型初始化 Map,例如 struct。
package mainimport ("fmt")type User struct {Name stringAge int}func main(){m2 := map[string]string{"a":"a"}m3 := make(map[string]string)// 结构体作为 valuem4 := map[string]User{"zhangsan": { "张三", 18},"lisi": {"李四", 19},}fmt.Println(m2, m3) // map[a:a] map[]fmt.Println(m4) // map[lisi:{李四 19} zhangsan:{张三 18}]}
对 Map 进行增删查改。
package mainimport "fmt"func main() {m := make(map[string]int)// 插入元素m["Answer"] = 42fmt.Println("The value:", m["Answer"])// 修改元素m["Answer"] = 48fmt.Println("The value:", m["Answer"])// 元素数量fmt.Println("The count:", len(m))// 删除元素delete(m, "Answer")fmt.Println("The value:", m["Answer"])// 判断某个key是否存在// 若 key 在 m 中,isset 为 true,否则 isset 为 false。// 若 key 不在映射中,那么 v 是该映射元素类型的零值。v, isset := m["Answer"]if(isset) {fmt.Println("v value is", v)}else{fmt.Println("v not found ", v)}}
对 Map 进行迭代时,它是随机输出元素的,如果你想顺序迭代,可以先对Map中的键排序,然后遍历排好序的键,把对应的值取出来。
package mainimport ("fmt""sort")func main() {m := map[string]string{"a": "A", "b": "B", "c": "C", "d": "D"}//===================随机取值=================for k, v := range m {fmt.Println(k, "=", v) // C D A b}//================= 顺序取值 ==================var keys []stringfor k := range m {keys = append(keys, k)}//对key进行排序sort.Strings(keys)for _, k := range keys {fmt.Println(k, "=", m[k]) // A b C D}}
new
import "fmt"type Student struct {name stringage int}func main() {// new 一个内建类型var a *inta = new(int)*a = 10fmt.Println(*a) //10// new 一个自定义类型s := new(Student)s.name = "bob"}
make
package mainimport "fmt"func main() {var b = make(map[string]string)b["name"] = "bob"fmt.Println(b)}
go 没有面向对象的概念,面向对象中我们使用
class关键字来申明一个类,而在 go 中我们可以通过type关键词来申明一个类型。可以将类型看作是面向对象中的对象(相当于申明了一个类)。
- 自定义类型:自定义类型是基于内置的基本类型,使用 type 关键字定义的一种全新类型,和原类型是不同的两个类型。
- 类型别名:类型别名和原类型完全一样,只不过是另一种叫法而已。
package maintype D = int // 类型别名,有等号type I int // 自定义类型,无等号func main() {v := 100var d D = v // 不报错var i I = v // 报错var K I = I(v) // 转换成 I 类型后就不报错了}
1.自定义类型
有这么一个闭包函数,参数类型和返回值类型都是函数,对于这样的代码其实是不好读的。
func getValue(func(n int) int) func(n int) int {return func(n int) int {return c(n) + 1}}
我们可以借助自定义类型重构如下,这样一来这个函数就比较可读了。
type IntCompute func(n int) intfunc getValue(c IntCompute) IntCompute {return func(n int) int {return c(n) + 1}}
2.类型别名
类型别名还可以为其它包中的类型定义别名,只要这个类型在其它包中是导出的:
package mainimport ("fmt""time")type MyTime = time.Timefunc main() {var t MyTime = time.Now()fmt.Println(t)}
如果类型别名导出是的,那么别的包中就可以使用,它和原始类型是否是导出没关系:
type t1 struct {S string}type T2 = t1
在switch中,你不能将原类型和类型别名作为两个分支,因为这是重复的case:
package mainimport "fmt"type D = intfunc main() {var v interface{}var d D = 100v = dswitch i := v.(type) {case int:fmt.Println("it is an int:", i)//报错: duplicate case int in type switchcase D:fmt.Println("it is D type:", i)}}
既然类型别名和原始类型是相同的,那么它们的方法集也是相同的:
package maintype T1 struct{}type T3 = T1func (t1 T1) say() {}func (t3 *T3) greeting() {}func main() {var t1 T1var t3 T3t1.say()t1.greeting()t3.say()t3.greeting()}
类型别名建立的类型只能为本地类型添加方法,不能为内置类型添加方法:
package mainimport ("fmt""time")type NTime = time.Time// 报错 :cannot define new methods on non-local type time.Timefunc (t NTime) Dida() {fmt.Println("嘀嗒嘀嗒嘀嗒嘀嗒搜索")}func main() {t := time.Now()t.Dida()}
package mainimport "fmt"//对象定义type person struct {name stringcity stringage int8}//构造函数func newPerson(name, city string, age int8) *person {return &person{name: name,city: city,age: age,}}//接收者func (p *person) SetAge(newAge int8) {p.age = newAge}func main() {p := newPerson("bob", "beijing", 20)p.SetAge(18)fmt.Println(p.name, (*p).age)}
Go语言中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了,Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体(struct),通过 struct 可以实现面向对象。
package mainimport ("fmt")func main() {//============================结构体定义=========================type T_simple struct {id intname stringphone stringage int}//基本的实例化var t T_simplet.id = 10t.name = "tcl"fmt.Println(t) //{10 tcl 0}//new 函数实例化new_simple = new(T_simple) //类似其他语言 new 对象一样t.id = 10t.name = "tcl"fmt.Println(t) //{10 tcl 0}//键值对方式实例化t_simple := T_simple{ id:11, name:"tcl"}fmt.Println(t_simple) // {11 tcl 0}//定义并初始化t_default := struct {Name stringAge int}{"tcl", 28,}fmt.Println(t_default) // tcl 28}
结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问),结构体成员使用 . 访问。
package mainimport "fmt"type Vertex struct {X intY int // 公开的z int // 私有的}func main() {v := Vertex{1, 2}v.X = 4fmt.Println(v.X) // 4}
指向结构体的指针 p,可以通过 (*p).X 来访问其字段 X,也可以通过 p.X 访问。p.X 的底层其实就是 (*p).X,这是go为了方便结构体使用帮我们实现的语法糖。
package mainimport "fmt"type Vertex struct {X intY int}func main() {v := Vertex{1, 2}p := &v(*p).X = 10p.Y = 20fmt.Println(v) // {10 20}}
方法只是个带接收者参数的函数,接收者位于 func 关键字和方法名之间,接收者只能是通过 type 关键词申明的类型。接收者的类型定义和方法声明必须在同一包内。
package mainimport ("fmt")type Rect struct {width float64height float64}// Area 方法拥有一个名为 r,类型为 Rect 的接收者func (r *Rect) Area() float64 {return r.width * r.height}//在Go语言中没有构造函数的概念,通常以 NewXXX 命名的函数来表示【构造函数】func NewRect(width, height float64) *Rect {return &Rect{width, height}}func main() {r := NewRect(3,4)fmt.Println(r.Area()) // 12}
如果结构体比较复杂的话,值拷贝性能开销会比较大,因此构造函数推荐返回结构体的指针类型。
方法调用时没有严格要求接收者的类型,即方法的接收者为指针类型的话,也可以通过值类型来调用该方法,反之也是这样。
一般来说指针接收者比值接收者更常用:
- 方法经常需要修改它的接收者指向的值。
- 可以避免在每次调用方法时复制该值,若值的类型为大型结构体时,这样做会更加高效。
package mainimport ("fmt")type person struct {name string}// 接收者为值func (p person) Call(){fmt.Println("value call")}// 接收者为指针func (p *person) Call2(){fmt.Println("point call")}func main() {p := person{}p.Call() // value call(&p).Call() // value callp.Call2() // point call(&p).Call2() // point call}
一个结构体中可以嵌套包含另一个结构体或结构体指针,我们称为结构体嵌套,这样就能将一个有简单行为的对象组合成有复杂行为的对像。
结构体还以可以包含一个或多个匿名结构体,即结构体没有字段名只有类型。匿名结构体其实仍然拥有自己的字段名,只是字段名就是其类型本身而己。
结构体嵌套
package mainimport "fmt"//Address 地址结构体type Address struct {Province stringCity string}//User 用户结构体type User struct {Name stringGender stringAddress Address}func main() {user := User{Name: "bob",Gender: "man",Address: Address{Province: "广东",City: "深圳",},}fmt.Printf("user=%#v\n", user)}
匿名结构体嵌套
嵌套多个匿名机结构体时,要求字段名称必须唯一,如果有同名字段会有冲突,如果结构体内部有同名字段,就不要使用匿名结构体嵌套了。
package mainimport "fmt"//Address 地址结构体type Address struct {Province stringCity string}//User 用户结构体type User struct {Name stringGender stringAddress //这里的}func main() {user := User{Name: "bob",Gender: "man",Address: Address{Province: "广东",City: "深圳",},}// 通过匿名结构体.字段名访问fmt.Println(user.Address.City)// 直接访问匿名结构体的字段名fmt.Println(user.Province)}
结构体标签是它是一个附属于字段的字符串,存储了字段的元数据(如字段映射,数据校验,对象关系映射等等)。
最常用的一个场景就是 json 的编解码,标签名会以结构体字段别名的方式出现在 json 中:
package mainimport ("encoding/json""fmt")// 在编解码时 "omitempty" 会忽略 false,0,空指针,空接口,空数组,空切片,空映射,空字符。// "-" 会完全跳过该字段。// 小写的字段也会被忽略。type User struct{Name string `json:"name"`Age int `json:"age"`Phone string `json:"phone,omitempty"`Id int `json:"-"`}func main() {h := User{Name : "tcl",Age : 27,Phone: "",Id : 100,}jsonData, err := json.Marshal(h)if err != nil {fmt.Println(err)}fmt.Println(string(jsonData)) //{"name":"tcl","age":27}}
字段的标签可以有多个键值对,多个键值对使用空格分开:
type User struct{Name string `json:"name"`Age int `json:"age"`Phone string `json:"phone,omitempty"`Id int `json:"-"`Area string `province:"广东" city:"shenzhen"`}
通过反射可以获取字段的标签信息:
package mainimport ("fmt""reflect")type User struct{Name string `json:"name"`Age int `json:"age"`Phone string `json:"phone,omitempty"`Id int `json:"-"`Area string `province:"广东" city:"shenzhen"`}func main() {var u Usert:=reflect.TypeOf(u)// 遍历字段for i:=0;i<t.NumField();i++{field:=t.Field(i)fmt.Println(field.Tag)}//获取特定字段的标签信息area, _ := t.FieldByName("Area")fmt.Println(area.Tag.Get("province")) //获取键值对中的某个 keyfmt.Println(area.Tag.Lookup("city")) //查找标签中某个 key 是否存在}
encoding/json 包中的 Marshal() 和 Unmarshal() 方法可以用来对json数据进行编解码, struct、slice、array、map都可以转换成json。
`Marshal()`:Go数据对象 -> json数据`UnMarshal()`:Json数据 -> Go数据对象
首字母小写的字段是不会被编码输出的(未导出),但如果你就想编码后的字段名是小写的,那么这时候结构体标签(json:tag)就能帮你实现这种效果。
package mainimport ("encoding/json""fmt""time")/*ref 是 Id 字段编解码时的别名private 字段不会被编码,因为它是小写(未导出的)*/type FruitBasket struct {Name stringFruit []stringId int64 `json:"ref"`private stringCreated time.Time}func main() {basket := FruitBasket{Name: "Standard",Fruit: []string{"Apple", "Banana", "Orange"},Id: 999,private: "Second-rate",Created: time.Now(),}jsonData, err := json.Marshal(basket)if err != nil {fmt.Println(err)}// json.Marshal 生成数据类型是字节数组([]byte),因此我们需要string()将其转换成字符串fmt.Println(string(jsonData))// "Name":"Standard","Fruit":["Apple","Banana","Orange"],"ref":999,"Created":"2019-07-27T15:13:5"}}
解码时,如果你明确知道待解码字符串的类型,你可以为 json 字符串指定一个解析模板,让其解析为特定的某个对象。
package mainimport ("encoding/json""fmt""time")type FruitBasket struct {Name stringFruit []stringId int64 `json:"ref"`private stringCreated time.Time}func main() {// json.Unmarshal 需要参数类型为字节数组, 需要把字符串转为 []byte 类型jsonStr := []byte(`{"Name": "Standard","Fruit": ["Apple","Banana","Orange"],"ref": 999}`)// 让 jsonStr 解析为对象 basketStructvar basketStruct FruitBasketerr := json.Unmarshal(jsonStr, &basketStruct)if err != nil {fmt.Println(err)}fmt.Println(basketStruct)}
JSON解析的时候只会解析能找得到的字段,找不到的字段会被忽略,这样的一个好处是:当你接收到一个很大的JSON数据结构而你却只想获取其中的部分数据的时候,你只需将你想要的数据对应的字段名大写,即可轻松解决这个问题。
如果你不知道待解码字符串有哪些字段,可以定义一个 interface{} 类型,interface{} 在 go 表示任意类型,然后利用类型断言特性var.(T),把解码结果转换为 map[string]interface{} 类型来进行后续操作。
package mainimport ("encoding/json""fmt""time")type FruitBasket struct {Name stringFruit []stringId int64 `json:"ref"`private stringCreated time.Time}func main() {jsonStr := []byte(`{"Name": "Standard","Fruit": ["Apple","Banana","Orange"],"ref": 999}`)// 申明一个任意类型的对象var any interface{}var basketStruct FruitBasketerr := json.Unmarshal(jsonStr, &basketStruct)err2 := json.Unmarshal(jsonStr, &any)if err != nil || err2 != nil {fmt.Println(err)}// interface{}()是类型转换的语法,因为类型断言要求变量类型必须接口类型,这里将 basketStruct 转换为接口类型value1, isBasketStruct := interface{}(basketStruct).(FruitBasket)fmt.Println(value1, isBasketStruct)// {Standard [Apple Banana Orange] 999 0001-01-01 00:00:00 +0000 UTC} true// 使用类型断言特性将结果转换为 map[string]interface{}value2, isMap := any.(map[string]interface{})fmt.Println(value2, isMap)//map[Fruit:[Apple Banana Orange] Name:Standard ref:999] true}
接口是由一组方法签名定义的集合,和传统面向对象语言中的接口一样,go 接口中的定义方法 只能是没有方法名的抽象方法,而通过接口中嵌入其它接口,就能实现传统面向对象中的接口继承。
接口也是一种类型,如果一个变量的类型是接口,我们就称该变量为接口型变量,可以用接口型变量保存任何实现了接口方法的对象。
package mainimport ("fmt")//=============== 接口申明 ============================type Abser interface {Abs() float64}//==============MyFloat 实现了接口 Abser==============type MyFloat float64func (f MyFloat) Abs() float64 {if f < 0 {return float64(-f)}return float64(f)}//==============Vertex 实现了接口 Abser ===================type Vertex struct {X, Y float64}func (v *Vertex) Abs() float64 {return v.X*v.X + v.Y*v.Y}func main() {//申明接口型变量var a Abservar b Abser// 接口类型的变量可以保存实现该接口的类型a = &Vertex{3, 4}b = MyFloat(-0.1)// 尽管方法名一样,但这两者的方法体行为完全不一样,这就是多态fmt.Println(a.Abs())fmt.Println(b.Abs())}
类型 Vertex 的方法 Abs(), 它的接收者是指针类型的 *Vertex ,不能把值类型的 Vertex 赋值给接口型变量,因为值类型的 Vertex 并未实现 Abser 接口。
但如果 Abs() 的接收者为值类型的 Vertex,那么指针类型的 *Vertex 是可以赋值给接口型变量。
func main() {var a Abservar c Absera = &Vertex{3, 4}c = Vertex{10, 20} //报错:Vertex does not implement Abser (Abs method has pointer receiver)fmt.Println(a.Abs())fmt.Println(c.Abs())}
嵌入的类型如果实现了接口,也视为当前类型实现了接口,即接口的方法,不一定需要由一个类型完全实现。
package mainimport ("fmt")type Servicer interface {a()b()}type serviceA struct{}func (sa serviceA) a() {fmt.Println("a")}// serviceB 尽管只实现了接口中的 b() 方法,但由于嵌入了 serviceA,则视 ServiceB 实现了 Service 接口type serviceB struct {serviceA}func (sb serviceB) b() {fmt.Println("b")}func main() {sv := serviceB{}v, isService := interface{}(sv).(Servicer)fmt.Println(v, isService)}
接口与接口间可以通过嵌套创造出新的接口。
type Servicer interface {discover()Loger}type Loger interface {log(driver interface{})}
空接口(interface{})是特殊形式的接口类型,普通的接口都有方法,而空接口没有定义任何方法。我们可以说所有类型都至少实现了空接口,任何类型值都可以赋值给空接口变量。因此,空 interface{} 代表任意类型(Any)。
package mainimport ("fmt")type Any interface {}// 接口作为函数参数和返回值func Ascll(anyParam Any) interface{}{if anyParam == "A" {return 65}else if anyParam == 65 {return "A"}// 任何类型值都可以赋值给空接口变量var anyValue interface{} = map[interface{}]Any{}return anyValue}func main() {fmt.Println(Ascll("A")) // 65fmt.Println(Ascll(65)) // Afmt.Println(Ascll(nil)) // map[]}
t, ok := obj.(T) 用来判断某个接口型变量是否属于某个类型并获取它的动态值,类型断言常用于对静态类型为空接口的对象进行断言。
某些情况下要断言的对象未必是接口类型,我们知道接口也是一种类型,因此可以通过 interface{}(obj)将其转换为接口型变量。
t, ok := interface{}(obj).(T)
package mainimport ("fmt")type It interface {Test()}type Person struct {name string}func (p Person) Test() {// todo}func main() {var a It = Person{name:"tcl"}val,isPerson := a.(Person)fmt.Println(val, isPerson) // {tcl} truevar b interface{} = nilval2,isPerson2 := b.(Person)fmt.Println(val2, isPerson2) // {} false}
i.(type) 类型选择用在断言多次的场景,其实就是将多个if判断的断言使用switch语句来实现。有点像别的语言中 get_type() 之类的函数。
类型选择中的声明与类型断言 i.(T) 的语法相同,只是具体类型 T 被替换成了关键字
type。
package mainimport "fmt"func do(i interface{}) {switch v := i.(type) {case int:fmt.Printf("Twice %v is %v\n", v, v*2)case string:fmt.Printf("%q is %v bytes long\n", v, len(v))default:fmt.Printf("I don't know about type %T!\n", v)}}func main() {do(21)do("hello")do(true)}
借助类型断言可以实现类型转换。
if r := recover(); r != nil {//通过类型断言,将捕获的错误值 r 转换为 string 类型,//然后传入到 errors.New 中返回 error 类型的消息str , ok := r.(string)if ok {err = errors.New(str)}}
空接口可以代表任意类型,借助类型断言的特性,我们可以将任何空接口值转换为我们需要的类型。
package mainimport ("fmt")func main() {var i interface{} = 100val := i.(int)fmt.Println(val + 100) // 200}
如果你构造一个变量时,不确定它的类型,可以先定义为 interface{} 进行赋值,后面用到该变量时进行类型断言转化为对应类型就行了。
var dbConfig = map[string]interface{}{"host" : "xxx.com:6379","passwd": "xxx","db" : 0,}func newPool(cfg interface{}) *redis.Pool {// dbConfig 中的每一项都是 interface{} 类型dbConfig := cfg.(map[string]interface{})host := dbConfig["host"].(string)passwd := redis.DialPassword(dbConfig["passwd"].(string))db := redis.DialDatabase(dbConfig["db"].(int))...}
使用类型断言修改对象值,断言的类型必须是指针。
package mainimport ("fmt")type User struct {id intname string}func main() {u := User{1, "Tom"}var vi, pi interface{} = u, &u //copy u and assign to vi// vi.(User).name = "Jack" // Error: cannot assign to vi.(User).namepi.(*User).name = "Jack"fmt.Printf("%v\n", vi.(User)) //{1 Tom}fmt.Printf("%v\n", pi.(*User)) //&{1 Jack}}
以接口组织代码的方式在 Go 语言中非常常见,较大的接口定义,可以由多个小接口定义组合而成。
type Reader interface {Read(p []byte) (n int, err error)}type Writer interface {Write(p []byte) (n int, err error)}type ReadWriter interface {ReaderWriter}//只依赖于必要功能的最小接口func StoreData(reader Reader) error {...}
面向接口是 Go 语言鼓励的开发方式,我们应该遵循固定的模式对外提供功能。
- 使用大写的 Service 对外暴露方法;
- 使用小写的 service 实现接口中定义的方法;
- 通过 func NewService(...) (Service, error) 函数初始化 Service 接口;
package posttype Service interface {ListPosts() ([]*Post, error)}type service struct {conn *grpc.ClientConn}func NewService(conn *grpc.ClientConn) Service {return &service{conn: conn,}}func (s *service) ListPosts() ([]*Post, error) {posts, err := s.conn.ListPosts(...)if err != nil {return []*Post{}, err}return posts, nil}
为我们提供一种可以在运行时操作任意类型对象的能力。比如我们可以查看一个接口变量的具体类型,看看一个结构体有多少字段,如何修改某个字段的值等等.
go 的 reflect 为我们提供了两个方法用来获取对象的类型和实例:
- reflect.TypeOf(): 获取任意对象的具体类型,如果为空则返回 nil。
- reflect.ValueOf():获取对象的实例,如果为空则返回 0。
package mainimport ("fmt""reflect")type User struct{Name stringAge int}func main() {user := User{"张三",20}// TypeOf会返回目标数据的类型,比如上文我们通过 type 关键字定义的 User 类型reflectType := reflect.TypeOf(user)// valueOf返回目标数据的的值,比如上文的 {张三 20}reflectValue := reflect.ValueOf(user)fmt.Println("type: ", reflectType)fmt.Println("value: ", reflectValue)}
底层类型指的是基础的数据类型,比如上面我们申明的 User 类型,其底层基础类型是 struct,底层类型可以通过 Kind() 方法获取。
package mainimport ("fmt""reflect")type User struct{Name stringAge int}func main() {user := User{"张三",20}reflectType := reflect.TypeOf(user)concreteType := reflectType.Kind()if concreteType == reflect.Struct {fmt.Println("底层类型是 struct")}else {fmt.Println("类型不是 struct")}}
NumField() 和 NumMethod() 可以获取目标对象的字段数量和方法数量。
package mainimport ("fmt""reflect")type User struct{Name stringAge int}func (u User) GetName() string{return u.Name}func main() {user := User{"张三",20}t := reflect.TypeOf(user)v := reflect.ValueOf(user)fmt.Println("类型名称:", t.Name())for i:=0;i<t.NumField();i++ {fmt.Println("字段名:",t.Field(i).Name)fmt.Println("字段值:",v.Field(i).Interface())}for i:=0;i<t.NumMethod() ;i++ {m := t.Method(i)fmt.Println("方法名:", m.Name)fmt.Println("方法类型:", m.Type)}}
利用反射修改对象的字段值时需要注意:
- 传入 ValueOf() 的是对象的地址,也就是对象的指针。
- Elem() 方法可以找到这个指针指向的值。
- FieldByName() 方法可以找到要修改的目标字段。
package mainimport ("fmt""reflect")type User struct {Name stringAge int}func main() {s := &User{Name: "张三", Age: 20}v := reflect.ValueOf(s)// 修改值必须是指针类型否则不可行if v.Kind() != reflect.Ptr {fmt.Println("不是指针类型,没法进行修改操作")return}// 获取指针所指向的元素v = v.Elem()if !v.CanSet() {fmt.Println("无法修改该对象")return}// 获取目标key的Value的封装name := v.FieldByName("Name")if name.Kind() == reflect.String {name.SetString("李四")}fmt.Printf("%#v \n", *s)// 如果是整型的话test := 888testV := reflect.ValueOf(&test)testV.Elem().SetInt(666)fmt.Println(test)}
package mainimport ("fmt""reflect")type User struct {Name stringAge int}func (s User) EchoName(name string){fmt.Println("我的名字是:", name)}func main() {s := User{Name: "张三", Age:20}v := reflect.ValueOf(s)// 获取目标方法m := v.MethodByName("EchoName")// 构造参数args := []reflect.Value{reflect.ValueOf("李四")}// 调用函数m.Call(args) //我的名字是: 李四}
更多关于反射的使用见 这里。
单线程也可以实现并发,go 中的并发由 Go 自身的调度器实现,并行是和主机的CPU 核数有关的,多核就可以并行并发,单核只能并发了。
协程本质上是一种用户态线程,不需要操作系统来进行抢占式调度,且在真正的实现中寄存于线程中,因此系统开销极小.
通过 go + func() 即可创建一个 goroutine,就这样一个简单的 go 关键字就将同步代码转为异步代码:
import "fmt"func mytest() {fmt.Println("hello, go")}func main() {// 启动一个协程go mytest()fmt.Println("hello, world")}
main 函数本身相当于主线程,当 main 函数执行完成后,这个线程也就终结了, 它不会等待其下运行着的所有协程返回结果,因此上面的这段代码只会输出hello, world ,而不会输出 hello, go。因为协程的创建需要时间,main 函数执行完后,它里面的协程可能还没来得及执行。
因此如果 main 函数等待几秒,则会看到其他协程的输出:
import ("fmt""time")func mytest() {fmt.Println("hello, go")}func main() {go mytest()fmt.Println("hello, world")time.Sleep(time.Second)}
WaitGroup 的字面意思就是指等待一组协程执行完成后才会继续向下执行。细节见这里这里
sync.WaitGroup 的使用步骤:
WaitGroup 的局限:
其中等待的一组协程中有一个协程发生错误,则告诉协程组的其他协程,全部停止运行(本次任务失败)以免浪费系统资源。该场景WaitGroup是无法实现的,那么该场景该如何实现呢,就需要用到通知机制,其实也可以用channel来实现。
func main() {var wg sync.WaitGroupgo func(){wg.Add(1)defer wg.Done()for i:=1;i<100;i++ {fmt.Println("A:",i)}}()go func(){wg.Add(1)defer wg.Done()for i:=1;i<100;i++ {fmt.Println("B:",i)}}()wg.Wait()}
通道用在多个协程间 同步数据。
注意:
waitGoup只是用来协程间同步,但不通信,而 channel 既能同步又能通信。
通道是和 slice,map 一样的数据类型,可以使用内置的 make 函数声明初始化。
# 定义通道ch:=make(chan int)# 读写通道ch <- 2<-ch# 关闭通道, 注意:通道只应该被发送者关闭。close(ch)# 检测通道是否关闭# data为通道的值,open 为通道的状态: 未关闭为 true, 已关闭则为 falsedata, open := <-pipline
通道读取有点像 php 的数组元素弹出函数 array_pop 一样,当你弹出一个元素后,源数组就少了一个,因此把通道读取也应看成弹出元素一样,比如说通道中只有一个元素,当你取完以后就不能再取了。
package mainimport ("fmt")func main() {ch := make(chan int, 1)go func() {ch <- 1}()fmt.Println(<-ch)fmt.Println(<-ch) //fatal error: all goroutines are asleep - deadlock!}
只能读或只能写的通道称为单项通道,单项通道一般用作函数参数。
- 只能写入的通道:ch chan <- int
- 只能读取的通道:ch <- chan int
// 只能写入func output(ch chan <- int) {//...}// 只能读取func input( ch <- chan int) {//...}
创建通道的 make 函数可以接收第二个参数,为通道的容量。
- 容量 = 0 的通道称为
无缓冲通道。无缓冲通道要求接收者和发送者不能处于同一个协程中,否则将会造成死锁,在接收者未读取通道前,发送者的后续执行都是阻塞的。- 容量 > 0 的通道称为
缓冲通道;对于缓冲通道,cap(ch)可以获取通道的容量,len(ch)可以获取通道中的元素数量,缓冲通道对接着者和发送者的顺序没有要求,并且二者可以出现在同一协程中。
无缓冲通道
package mainimport ("fmt""time")func hello(pipline chan string) {pipline <- "hello world"}func main() {pipline := make(chan string)go hello(pipline) //发送者fmt.Print(<-pipline) //接收者time.Sleep(time.Second)}
需要注意,无缓冲通道会阻塞后续的执行,直到通道中的数据被读取。
package mainimport ("fmt""time")func hello(ch chan string) {ch <- "hello world\n"//阻塞后续代码的执行,顺序输出 hello world 和 endfmt.Print("end\n")}func main() {//未设置容量的非缓冲通道ch := make(chan string)go hello(ch)time.Sleep(time.Millisecond * 100)fmt.Print(<-ch)//等待几秒以便子协程中的数据输出time.Sleep(time.Millisecond * 100)/*hello worldend*/}
缓冲通道
package mainimport "fmt"func main() {pipline := make(chan string, 1)pipline <- "hello world"fmt.Println(<-pipline)//hello world}
缓冲通道不会阻塞后续代码的执行,你会看到后续代码会立马输出。
package mainimport ("fmt""time")func hello(ch chan string) {ch <- "hello world\n"//不会阻塞后续代码执行,你会看到 end 先于 hello world 输出fmt.Print("end\n")}func main() {//设置了容量的缓冲通道ch := make(chan string, 1)go hello(ch)time.Sleep(time.Millisecond * 100)fmt.Print(<-ch)/*endhello world*/}
通道遍历可以有三种方式: for,for-range,任务数量;前两种遍历通道时,务必确保发送者已关闭(close)通道,否则会造成接收者一直在等待数据 ,造成死锁;最后一种则无需关闭通道, 但缺点是你必须知道任务数量是多少。
package mainimport "fmt"func main() {taskNum := 10c := make(chan int, taskNum)//c := make(chan int)// 发送者go func() {for i := 0; i < taskNum; i++ {c <- i}//关闭channelclose(c)}()//第一种:for循环遍历for {if data, ok := <-c; ok {fmt.Println("for:", data)} else {break}}//第二种:for-range遍历for i := range c {fmt.Println("for-range:", i)}//第三种:通过任务数量遍历,这种发送者无需关闭 channelfor i := 0; i < taskNum; i++ {fmt.Println("taskNum:", <-c)}}
再看一个通道遍历的变体,在协程中遍历通道。
package mainimport ("fmt")func main() {c := make(chan int)execRes := make(chan string, 10)//协程1: forgo func() {for {execRes <- fmt.Sprintf("task=%d exec by for", <-c)}}()//协程2: for-rangego func() {for i := range c {execRes <- fmt.Sprintf("task=%d exec by for-range", i)}}()// 生成10个任务,分配给给两个协程去执行taskNum := 10for i := 1; i <= taskNum; i++ {c <- i}// 通过任务数量遍历通道,获取每一个任务的执行结果,每个协程最终能分配几个任务是随机的for i := 0; i < taskNum; i++ {fmt.Println(<-execRes)}}
select 会阻塞到某个 channel 可以继续执行为止,当多个 channel 就绪时,select 会随机选择一个执行。
select 的语法类似 switch 的功能,可以使用 break 或 return 就会退出 select 语句。
c := make(chan int)go func() {for v := range c {// 随机输出了0,1, 证明select 是随机读取/写入通道的fmt.Println(v)}}()for i := 0; i < 10; i++ {select {case c <- 0:case c <- 1:}}
为了防止长时间阻塞,可以为 select 设置超时时间(等待 channel 数据的时间)。
c := make(chan bool)select {case v := <-c:fmt.Println(v)case t := <-time.After(3 * time.Second):fmt.Println(t)fmt.Println("Timeout")}
为了在发送或者接收时不发生阻塞,select 语句还支持定义一个 default 分支。
c := make(chan int)select {case i := <-c:fmt.Println(i)default:fmt.Println("default")}
无论缓冲/非缓冲通道,如果接收者出现在发送者前面会造成死锁,因为接收者为了读取数据会一直阻塞后面的发送者往通道里写数据。
//死锁func main() {// c := make(chan int, 1)c := make(chan int)//通道中没有数据会一直阻塞后续的执行fmt.Println(<-c)go func() {c <- 1}()//fatal error: all goroutines are asleep - deadlock!}//正常func main() {// c := make(chan int, 1)c := make(chan int)go func() {c <- 1}()//通道中能够读取数据fmt.Println(<-c)//1}
非缓冲通道,发送者出现在前面 也会造成死锁,因为通道无法被读取,导致发送者一直阻塞;但如果是 缓冲通道则正常,因为缓冲通道写入不会阻塞后续执行。
//死锁(非缓冲通道)func main() {c := make(chan int)//通道无法被读取,会一直阻塞后续的执行c <- 1go func() {fmt.Println(<-c)}()time.Sleep(3 * time.Second)// fatal error: all goroutines are asleep - deadlock!}//正常(非缓冲通道)func main() {c := make(chan int)go func() {fmt.Println(<-c)}()//通道能被读取,不会阻塞后续执行c <- 1time.Sleep(3 * time.Second)//1}//正常(缓冲通道)func main() {c := make(chan int, 1)//缓冲通道不会阻塞后续执行c <- 1go func() {fmt.Println(<-c)}()time.Sleep(3 * time.Second)// 1}
无论缓冲/非缓冲通道,在同一协程中 通道写入超过通道容量限制会导致死锁;但如果不在同一协程,写入数据超过通道容量限制则不会造成死锁。
//正常:未超过容量限制func main() {pipline := make(chan string, 1)pipline <- "hello world"fmt.Println(<-pipline)}//死锁:超过容量限制func main() {ch1 := make(chan string, 1)ch1 <- "hello world"ch1 <- "hello China" //这条写入超过容量限制,导致死锁fmt.Println(<-ch1)//fatal error: all goroutines are asleep - deadlock!}//正常:不在同一协程写入超过容量限制package mainimport "fmt"func main() {ch := make(chan string, 1)go func() {ch <- "hello world"ch <- "hello China"}()fmt.Println(<-ch)//hello world}
无论缓冲/非缓冲通道,如果通道没有关闭,读取次数超过通道容量会造成死锁;但如果通道关闭的话,可以随意读取多次,如果值已取出,则读取的是类型的零值。
package mainimport "fmt"func main() {UnDeadLocl()DeadLocl()}//死锁func DeadLocl() {ch := make(chan int, 1)go func() {ch <- 1//close(ch)}()fmt.Println(<-ch)fmt.Println(<-ch)}//正常func UnDeadLocl() {ch := make(chan int, 1)go func() {ch <- 1close(ch)}()fmt.Println(<-ch)fmt.Println(<-ch)}
遍历一个未关闭的缓冲/非缓冲通道,会造成死锁,为了避免这种情况,发送者在发送完数据后,需要调用 close() 来关闭通道。但如果你通过任务数量来遍历通道,则无需关闭通道。
package mainimport "fmt"func main() {DeadLocl()UnDeadLocl()}//死锁func DeadLocl() {pipline := make(chan string, 2)go func() {pipline <- "hello world"pipline <- "hello China"// close(pipline)}()for data := range pipline {fmt.Println(data)}}//正常(无需close通道)func UnDeadLocl() {taskNum := 2ch := make(chan string, taskNum)go func() {ch <- "hello world"ch <- "hello China"// close(pipline)}()for i := 0; i < taskNum; i++ {fmt.Println(<-ch)}}
用通道来做锁 :使用容量为1的缓冲通道以达到共享变量的目的,当通道达到设定的容量时,此时再往里发送数据会阻塞整个程序。
package mainimport ("fmt""time")func main() {// 注意要设置容量为 1 的缓冲信道以达到共享变量的目的ch := make(chan bool, 1)var x intfor i := 0; i < 1000; i++ {go func(n int) {//如果使用非缓冲通道,在同一协程中将出现死锁(发送者和接收者相互等待),将导致永远执行不到x++ 这里ch <- true// 由于 x++ 不是原子操作,所以应避免多个协程对x进行操作// 使用容量为1的信道可以达到锁的效果x++<- ch}(x)}time.Sleep(time.Second)fmt.Println("x 的值:", x)}
退出信号。有并发就有资源竞争,两个或者多个goroutine在没有相互同步的情况下,访问某个共享的资源,比如同时对该资源进行读写时,就会处于相互竞争的状态。
下面一个资源竞争的例子,我们可以多运行几次这个程序,会发现结果可能是2,也可以是3,也可能是4。因为共享资源count变量没有任何同步保护,所以两个goroutine都会对其进行读写,会导致对已经计算好的结果覆盖。
package mainimport ("fmt""runtime""sync")var (count int32wg sync.WaitGroup)func main() {wg.Add(2)go incCount()go incCount()wg.Wait()fmt.Println(count)}func incCount() {defer wg.Done()for i := 0; i < 2; i++ {value := countruntime.Gosched()value++count = value}}
借助 go build -race 我们可以详细的查看 goroutine 间的竞争情况。它会为我们在当前项目下生成一个可以执行文件,然后我们运行这个可执行文件,就可以看到打印出的检测信息。
tcl@tcl:/data/goProject$ go build -racetcl@tcl:/data/goProject$ ./goProject==================WARNING: DATA RACERead at 0x0000005ef5ec by goroutine 7:main.incCount()/data/goProject/index.go:25 +0x6fPrevious write at 0x0000005ef5ec by goroutine 6:main.incCount()/data/goProject/index.go:28 +0x8eGoroutine 7 (running) created at:main.main()/data/goProject/index.go:17 +0x77Goroutine 6 (running) created at:main.main()/data/goProject/index.go:16 +0x5f==================4Found 1 data race(s)
可以看到提示我们:
goroutine 7在代码25行读取共享资源value := count,而这时goroutine 6正在代码28行修改共享资源count = value,这就出现了资源竞争了。
面对并发类的资源竞争问题,传统解决的办法就是对资源加锁,那么接下来我们看下 go 中提供的几种资源竞争的处理方案。
atomic包中的原子函数提供了对资源加锁的功能。
atomic.LoadInt32和atomic.StoreInt32两个函数,一个读取int32类型变量的值,一个是修改int32类型变量的值,这两个都是原子性的操作,Go已经帮助我们在底层使用加锁机制,保证了共享资源的同步和安全。
package mainimport ("fmt""runtime""sync""sync/atomic")var (count int32wg sync.WaitGroup)func main() {wg.Add(2)go incCount()go incCount()wg.Wait()fmt.Println(count)}func incCount() {defer wg.Done()for i := 0; i < 2; i++ {value := atomic.LoadInt32(&count)runtime.Gosched()value++atomic.StoreInt32(&count,value)}}
使用原子函数后,再次使用工具 go build -race 检查,发现不会提示有问题了。
tcl@tcl:/data/goProject$ go build -racetcl@tcl:/data/goProject$ ./goProject2
atomic虽然可以解决资源竞争问题,但是比较都是比较简单的,支持的数据类型也有限,所以Go语言还提供了另一种类型的锁-互斥锁。
互斥锁可以让我们自己灵活的控制哪些代码,同时只能有一个goroutine访问,被互斥锁控制的这段代码范围,被称之为临界区,临界区的代码,同一时间,只能又一个goroutine访问。
package mainimport ("fmt""runtime""sync")var (count int32wg sync.WaitGroupmutex sync.Mutex)func main() {wg.Add(2)go incCount()go incCount()wg.Wait()fmt.Println(count)}func incCount() {defer wg.Done()for i := 0; i < 2; i++ {mutex.Lock()value := countruntime.Gosched()value++count = valuemutex.Unlock()}}
示例中我们先调用mutex.Lock()对有竞争资源的代码加锁,这样当一个goroutine进入这个区域的时候,其他goroutine就进不来了,只能等待,一直到调用mutex.Unlock() 释放这个锁为止。
互斥锁有个问题:
当一个goroutine访问的时候,其他goroutine都不能访问,这样肯定保证了资源的同步,避免了竞争,不过也降低了性能。
而我们希望是像 mysql 中的共享锁一样,当前进程读数据的时候,不要互斥其他的进程的读操作,因为数据不会被修改,那么其实是不存在资源竞争的问题的,不管怎么读取,多少goroutine同时读取,都是可以的那么。
所以这就延伸出来 go 中另外一种锁-读写锁:
读写锁可以让多个读操作同时并发,同时读取,但是对于写操作是完全互斥的。也就是说,当一个goroutine进行写操作的时候,其他goroutine既不能进行读操作,也不能进行写操作。
var count intvar wg sync.WaitGroupvar rw sync.RWMutexfunc main() {wg.Add(10)for i:=0;i<5;i++ {go read(i)}for i:=0;i<5;i++ {go write(i);}wg.Wait()}func read(n int) {rw.RLock()fmt.Printf("读goroutine %d 正在读取...\n",n)v := countfmt.Printf("读goroutine %d 读取结束,值为:%d\n", n,v)wg.Done()rw.RUnlock()}func write(n int) {rw.Lock()fmt.Printf("写goroutine %d 正在写入...\n",n)v := rand.Intn(1000)count = vfmt.Printf("写goroutine %d 写入结束,新值为:%d\n", n,v)wg.Done()rw.Unlock()}
我们在read里使用读锁,也就是RLock和RUnlock,写锁的方法名和我们平时使用的一样Lock和Unlock,这样我们就使用了读写锁,可以并发的读,但是同时只能有一个写,并且写的时候不能进行读操作。
sync.Cond 经常用在多个 goroutine 等待,一个 goroutine 通知(事件发生)的场景。
sync.Cond的作用并不是保证在同一时刻仅有一个线程访问某一个共享数据,而是在对应的共享数据的状态发生变化时,通知其他因此而被阻塞的协程。
sync.Cond 需要与锁(互斥锁,或者读写锁)一起使用, NewCond 函数可以返回 *sync.Cond 的对象指针,成员变量 L 代表与条件变量搭配使用的锁。
func NewCond(l Locker) *Cond {return &Cond{L: l}}// A Locker represents an object that can be locked and unlocked.type Locker interface {Lock()Unlock()}
该对象主要有三个方法
- wait():阻塞等待通知,如源码所示Wait()方法会阻塞所在协程,直到收到其他协程的通知结束阻塞时,会重新给 c.L 加锁,并且继续执行 Wait 后面的代码。因此调用 Wait 方法前,必须加锁。
- Signal():单发通知,给一个正等待(阻塞)在该条件变量上的 goroutine 发送通知。
- Broadcast():广播通知,给正在等待(阻塞)在该条件变量上的所有 goroutine 发送通知。
func (c *Cond) Wait() {c.checker.check()t := runtime_notifyListAdd(&c.notify) // 等待的goruntine数+1c.L.Unlock() // 释放锁资源runtime_notifyListWait(&c.notify, t) // 阻塞,等待其他goruntine唤醒c.L.Lock() // 获取资源}func (c *Cond) Signal() {c.checker.check()runtime_notifyListNotifyOne(&c.notify) // 唤醒最早被阻塞的goruntine}func (c *Cond) Broadcast() {c.checker.check()runtime_notifyListNotifyAll(&c.notify) // 唤醒所有goruntine}
示例一
package mainimport ("fmt""sync""time")var (lock sync.Mutex //互斥锁cond = sync.NewCond(&lock) //条件变量)func main() {//调用 Wait 方法前必须加锁cond.L.Lock()fmt.Println("main thread got lock ")go doSomething() //子线程fmt.Println("main thread begin wait ")//main 线程执行到 wait 释放锁后,这时子线程会获取锁成功cond.Wait()fmt.Println("main thread end wait ")cond.L.Unlock()fmt.Println("main thread release lock ")/*main thread got lockmain thread begin waitsub thread begin get locksub thread got locksub thread release lockmain thread end waitmain thread release lock*/}func doSomething() {time.Sleep(time.Second * 2)fmt.Println("sub thread begin get lock ")//调用条件变量的signal方法之前,加锁这个并不是必须的,可以注释获取和释放锁代码,也是OK的//cond.L.Lock()//defer cond.L.Unlock()fmt.Println("sub thread got lock ")time.Sleep(5 * time.Second)cond.Signal() //这时候main线程则会重新获取到锁,然后从wait返回fmt.Println("sub thread release lock")}
示例二
假设我们需要循环处理某队列中的任务,但是我们又想在没有任务时,能够出让CPU的空闲给别的进程。
未使用条件变量前的代码:
func addTask(t *Task) {mutex.Lock()tasks[t.Id] = tmutex.UnLock()}func Loop() {for {mutex.Lock()for taskId, task := range tasks {// handle code}lenght := len(task)mutex.UnLock()// 为了减少cpu空转,当队列为空的时候sleep 2秒if length == 0 {time.Sleep(time.Second * 2)}}}
使用条件变量后的代码:
func addTask(t *Task) {mutex.Lock()tasks[t.Id] = tif len(task) == 1 {cond.Signal()}mutex.UnLock()}func Loop() {for {mutex.Lock()// 如果当前任务数为0 调用Wait()等待新任务,直到添加任务后被唤醒if len(tasks) == 0 {cond.Wait()}for taskId, task := range tasks {// handle code}mutex.UnLock()}}
sync.Once典型应用场景就是仅需执行一次的任务,例如数据库连接池的建立,全局变量的延迟初始化等。
sync.Once 类型的 Do() 可以实现只执行一次的效果,该方法接收一个无参数,无结果的函数作为参数,该方法一旦被调用,就会去调用作为参数的那个函数。
package mainimport ("fmt""math/rand""sync")func main() {var count = 0var once sync.Oncemax := rand.Intn(100)for i:=0 ; i<max; i++{once.Do(func() {count++})}fmt.Println("Count:",count) // Count: 1}
可以看到无论运行了多少次,始终输出的是第一次运行的结果 Count: 1。
一句话总结:保存和复用临时对象,减少内存分配,降低 GC 压力。
sync.Pool 只有一个公开的字段 New,该字段的类型是一个函数类型,该函数一般仅在池中无可用对象的时候才调用,使用 New 生成的对象是持久的。
type Pool struct {noCopy noCopylocal unsafe.Pointer // 本地P缓存池指针localSize uintptr // 本地P缓存池大小// 当池中没有可能对象时// 会调用 New 函数构造构造一个对象New func() interface{}}
sync.Pool 还有两个方法 Put 和 Get ,前者用来向对象池中写入对象,后者用来从对象池中读取对象。
Put 方法写入的对象是临时的,因为这种对象通过 Get 方法访问时,从对象池中返回的同时并删除,并且这种临时对象也会受垃圾回收的影响。
对于 Get 方法,如果没有临时对象,那么就会访问 New 函数生成的持久对象,如果你在初始化对象池的时候没有设置 New 函数,那么 Get 方法只能返回 nil 了。
注意:
Get 方法只会删除 Put 方法写入的临时对象,并不会删除 New 函数创建的持久对象。
示例
package mainimport ("fmt""runtime""runtime/debug""sync""sync/atomic")func main() {// 禁用GC,并保证在main函数执行结束前恢复GC。defer debug.SetGCPercent(debug.SetGCPercent(-1))var count int32newFunc := func() interface{} {return atomic.AddInt32(&count, 1)}pool := sync.Pool{New: newFunc}// 访问使用 New 函数创建的持久对象v1 := pool.Get()fmt.Printf("Value 1: %v\n", v1) //1// 临时对象池的存取pool.Put(10)pool.Put(11)pool.Put(12)v2 := pool.Get()fmt.Printf("Value 2: %v\n", v2) // 10// 手动触发垃圾回收,导致之前使用 put 写入的三个值被回收掉debug.SetGCPercent(100)runtime.GC()// 这时再访问对象池,就会返回 New 函数创建的对象v3 := pool.Get()fmt.Printf("Value 3: %v\n", v3) // 2//置空对象池,这时再获取对象就会发现返回为 nilpool.New = nilv4 := pool.Get()fmt.Printf("Value 4: %v\n", v4) // nil}
Context 是一个可以帮助我们实现多 goroutine 协作流程的同步工具,从设计的角度来看,Context 提供了一种
父goroutine对子goroutine的管理功能。
我们为某个 HTTP 请求创建一个 goroutine 以并发地处理业务,同时这个 goroutine 也可能会创建更多的 goroutine 来访问数据库或者 RPC 服务。当上层的 goroutine 超时或者被终止的时候,我们希望其他的 子goroutine 及时停掉无用的工作减少额外资源的消耗。
这其实就是Context的最大作用,如果没有Context,当上层执行的操作出现错误时,下层其实不会收到错误而是会继续执行下去。每一个 Context 都会从最顶层的 goroutine 一层一层传递到最下层,这也是 Context 最常见的使用方式,此外 Context 还能携带键值对信息传递给下层 goroutine。
Context 是 context 包对外暴露的接口,该接口定义了四个需要实现的方法:
type Context interface {Deadline() (deadline time.Time, ok bool)Done() <-chan struct{}Err() errorValue(key interface{}) interface{}}
Deadline方法是获取设置的截止时间的意思,第一个返回式是截止时间,到了这个时间点,Context会自动发起取消请求;第二个返回值ok==false时表示没有设置截止时间,如果需要取消的话,需要调用取消函数进行取消。
Done方法返回一个只读的chan,类型为struct{},我们在goroutine中,如果该方法返回的chan可以读取,则意味着parent context已经发起了取消请求,我们通过Done方法收到这个信号后,就应该做清理操作,然后退出goroutine,释放资源。多次调用 Done 方法会返回同一个 Channel。
Err 方法可以获取取消的错误原因,因为什么Context被取消。
Value方法获取该Context上绑定的值,是一个键值对,所以要通过一个Key才可以获取对应的值,这个值一般是线程安全的。
父 Context
在代码中,都是基于父Context(也就是 根Context) 派生出 子context。这些 Context 对象形成一棵树:当一个 Context 对象被取消时,继承自它的所有 Context 都会被取消
有两种方法创建 父Context:
- context.Background()
- context.TODO()
这两个方法只能用在最外层的 goroutine 代码中,比如 main 函数里。一般使用 Background() 方法创建父Context。
TODO() 用于当前不确定使用何种 Context,留待以后调整。
子 Context
一个 Context 被 cancel,那么它派生出的 Context 都会收到取消信号(表现为 context.Done() 返回的 channel 收到值)。
有四种方法派生 context :
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)func WithValue(parent Context, key, val interface{}) Context
这四个 With 函数,接收的都有一个 partent 参数,就是 父Context,我们要基于这个父Context 创建出子Context 的意思,这种方式可以理解为子Context对父Context的继承,也就是基于父Context 的衍生。
可以看到前三个函数都返回一个取消函数 CancelFunc,这个函数的主要作用就是在 parent 和 child 之间同步取消和结束的信号,保证在 parent 被取消时,child 也会收到对应的信号,不会发生状态不一致的问题。
WithTimeout和WithDeadline基本上一样,表示多少时间后自动取消Context的意思,然我们也可以不等到这个时候,可以提前通过取消函数进行取消.
WithValue 函数和取消Context无关,它会创建一个携带信息的子context,可以是 user 信息、认证 token等。该 Context 与其派生的子Context 都会携带这些信息。
示例 context.WithCancel
package mainimport ("context""fmt""time")func main() {ctx, cancel := context.WithCancel(context.Background())go func(ctx context.Context) {for {select {case <-ctx.Done():fmt.Println("收到取消任务的通知了...")returndefault:fmt.Println("执行任务中...")time.Sleep(1 * time.Second)}}}(ctx)time.Sleep(3 * time.Second)fmt.Println("任务执行出错,通知其他 goroutine 取消任务")cancel()time.Sleep(3 * time.Second)fmt.Println("完毕...")/*执行任务中...执行任务中...执行任务中...任务执行出错,通知其他 goroutine 取消任务收到取消任务的通知了...完毕...*/}
示例 WithTimeout
package mainimport ("context""fmt""time")func main() {timeOut := 1 * time.Secondch := make(chan string)ctx, _ := context.WithTimeout(context.Background(), timeOut)go handle(ctx, ch)fmt.Println(<-ch)/*开始运行...超时退出...*/}func handle(ctx context.Context, ch chan string) {fmt.Println("开始运行...")time.Sleep(1 * time.Second)select {case <-ctx.Done():ch <- "超时退出..."default:ch <- "结束运行..."}}
上述示例中如果你将 timeOut 换成2s后你会发现它是正常输出的.
示例 WithValue
package mainimport ("context""fmt""time")var key string = "name"func main() {ctx, cancel := context.WithCancel(context.Background())//这样就生成了一个新的 Context,这个新的 Context 会带有键值对valueCtx:=context.WithValue(ctx,key,"custom-value")go watch(valueCtx)//sleep 2s 以便等待协程输出后再输出time.Sleep(2 * time.Second)fmt.Println("任务执行出错,通知其他 goroutine 取消任务")//取消父Contextcancel()//sleep 2s 以便等待协程输出后再输出time.Sleep(2 * time.Second)fmt.Println("完毕...")}func watch(ctx context.Context) {for {select {case <-ctx.Done()://取出值fmt.Println("收到取消任务的通知了,value:", ctx.Value(key))returndefault://取出值fmt.Println("执行任务中,value:",ctx.Value(key))time.Sleep(2 * time.Second)}}}/*执行任务中,value: custom-value任务执行出错,通知其他 goroutine 取消任务收到取消任务的通知了,value: custom-value完毕...*/