• Go语言基础学习


    Go基础语法(持续更新中...)

    Tips

    js解析整数为浮点数时,int64可能溢出

    type Request struct {
      ID int64 `json:"id,string"` //加上 tag:json:"id:string"
    }
    

    异常报错

    // 1. 数组/切片索引越界 
    panic: runtime error: index out of range [x] with length x
    // 2. 空指针调用
    panic: runtime error: invalid memory address or nil pointer dereference
    // 3. 过早关闭HTTP响应体
    panic: runtime error: invalid memory address or nil pointer dereference
    // 4. 除以0
    panic: runtime error: integer divide by zero
    // 5. 向已关闭的chan发送s信息
    panic: send on closed channel
    // 6. 重复关闭chan
    panic: close of closed channel
    // 7. 关闭未初始化的通道
    panic: close of nil channel
    // 8. 未初始化map
    panic: assignment to entry in nil map
    // 9. 跨协程的异常处理
    panic: runtime error: integer divide by zero
    // 10. sync计数为负值
    panic: sync: negative WaitGroup counter
    

    变量

    // 方法1
    var y int             // 声明变量类型
    var x string = "abc"  // 声明类型并赋值
    var s1, s2 = "he", 12 // 一行定义变量
    
    // 方法2
    var(
        a string  // 默认值为空字符串
        b int     // 默认值为0
        c bool    // 默认值为false
        d = "ABC" // 字符串赋值必须是双引号!
    )
    
    // 方法3
    func h() {
        a1, b1, c1 := 1, "str", true // 声明变量并赋值,只能在函数内使用:= 推荐
    }
    

    常量(定义后不可变的值)

    在 Go 语言中,只允许布尔型、字符串、数字类型这些基础类型作为常量。

    1. iota 枚举,只能在常量中使用,iota默认是0
    2. 遇到const iota就初始化为零
    3. const中每新增一行变量声明iota就递增1
    4. const声明如果不写,默认就和上一行一样
    const filename = "abc.log" // 定义常量
    const(
        A = iota + 1 // 输出1,也可以减n把初始值变成负数
        B            // 输出2
        _            // 单下划线表示跳过值
        C            // 输出4
    )
    

    数据类型

    字符串(string)

    小结

    使用双引号表示字符串
    使用单引号表示字符
    ``表示换行字符串,里面使用(转义符号),不生效,表示字符串

    字符串格式化

    %s // 表示字符串
    %q // 打印原始字符串
    %p // 显示内存地址,指针的格式化标识符为%p
    %d // 整数
    %c // 表示字符
    %t // 布尔值
    %T // 查看变量类型
    %b // 打印二进制
    %f // 表示浮点数
    %% // 可以打印百分号,用%转以
    %v // 默认占位符,值的默认格式表示
    %+v // 类似%v,但输出结构体时会添加字段名
    %#v // 完全打印所有的字段,可以打印结构体每个成员的名字
    

    字符串处理

    package main
    
    import (
        "fmt"
        "strings"
    )
    
    var s = `多行
    文本
    字符串
    `
    var(
        s0 = "[abc,def,hig]"
        s1 = "My name is"
        s2 = "Age is"
        a1 = '你' //单引号定义字符
    )
    
    //自定义函数,分割多个字符的
    func SplitByMoreStr(r rune) bool {
    	return r == '[' || r == ']' || r == ',' || r == '"'
    }
    
    func main() {
        //字符串拼接
        fmt.Println(s1 + "cby" + " " + s2 + "26")
        me := fmt.Sprintf("%s cby %s 26",s1,s2)
        fmt.Println(me)
        
        //分割
        fmt.Println(strings.Split(s1, "")) //默认会把每一个字符串元素分割成切片
        
        //分割多个字符
        x := strings.FieldsFunc(s0, SplitByMoreStr)
        
        //判断包含,相当于python的 "xxx" in s
        fmt.Println(strings.Contains(s1, "name"))
        
        //判断前后缀字符串
        fmt.Println(strings.HasPrefix(s1, "My")) //判断前,返回布尔
        fmt.Println(strings.HasSuffix(s1, "is")) //判断后,返回布尔
        
        //判断元素的位置
        s3 := "apendp"
        fmt.Println(strings.Index(s3,"p"))
        fmt.Println(strings.LastIndex(s3,"p"))
        
        //切片分割成字符串
        s4 := []string{"python", "php", "Go"}
        fmt.Println(strings.Join(s4,"-")) //把切片用"-"连接成一个字符串
    
        //for range 循环 是按照rune类型去遍历的
        s5 := "hello 世界"
        for _,s := range s5{
            fmt.Printf("%c
    ",s)
        }
    }
    

    字符串反转

    package main
    import "fmt"
    
    func main() {
       s := "hello"
       s1 := []byte(s) // 使用[]rune(s)也可以
       r := ""
       for i := len(s1) - 1; i >= 0; i-- {
          r += string(s1[i])
       }
       fmt.Println(r)
    }
    

    修改字符串

    要修改字符串需要将其转化成 []rune 或者 []byte,完成后在转换成string,无论哪种转换都会重新分配内存,并复制字节数组。

    package main
    import "fmt"
    
    func main() {
       s1 := "big"
       b := []byte(s1)
       b[0] = 'p'
       fmt.Println(string(b)) //输出 pig
    
       s2 := "你好"
       r := []rune(s2)
       r[0] = '和'
       fmt.Println(string(r)) //输出 和好
    }
    

    byte和rune

    英文字符用byte(ASCII码表示) 010101rune(中文,UTF8编码)
    rune类型专门处理Unicode

    整数型(int)

    // 有符合整型,可以是复数、零和整数。
    int     没有具体bit大小的整数,根据硬件设备cpu有关
    int8    范围 -128到127
    int16   范围 -32768到32767
    int32   范围 -2147483648到2147483647
    int64   范围 -9223372036854775808到9223372036854775807
    // 无符合整型,只能是零和整数。
    uint    没有具体bit大小的整数,根据硬件设备cpu有关
    uint8   范围 0到255
    uint16  范围 0到65535
    uint32  范围 0到4294967295
    uint64  范围 0到18446744073709551615
    

    浮点型

    float32 flot64 浮点数都是不精确的。
    浮点运算方法:
    1.转化成字符串去做运算
    2.整体放大多少倍换成整数进行运算

    复数(不常用)

    complex64 
    complex128
    

    逻辑运算与判断循环

    逻辑运算符

    && // 并且,返回布尔值
    || // 并且有一个成立就为真,返回布尔值
    !  // 原来为真取非,原来为假则为真
    if !false || !false {pass} // 判断布尔值
    

    判断语法

    if {
    } else if {
    } else {
    }
    

    switch语句

    switch x {
        case "a","b":
            fmt.Println("1")
        case x>10 && x<20:
            fmt.Println("1")
        default:
            fmt.Println("2")
    

    for循环

    for i:=0;i<=10;i++ {
        // pass
    }
    // 死循环
    for {
        // pass
    }
    // for range循环
    for i,x := range []int{1,2,3} {
        // pass
    }
    

    数组

    当把一个数组作为参数传入函数的时候,传入的其实是该数组的拷贝,而不是它的指针。如果要使用指针,需要 slice 类型。
    数组必须都是同一类型的元素,长度也是类型的一部分。
    数组的长度是数组类型的一个组成部分,因此[3]int和[4]int是两种不同的数组类型。

    package main
    import "fmt"
    
    var a [5]int //定义数组a,方法1
    var b = [...]int{1,2,3,5,8} //定义并赋值数组,方法1
    
    var(
       a [5]int //定义数组a,方法2
       s = [2]string{"name","age"} //定义并赋值数组,方法2
       b = [...]int{1,2,3,5,8}     //定义数组可以使用...省略
    )
    func main()  {
       a = [5]int{1,2,3,4,5} //赋值数组,方法1
       a[0] = 9              //赋值数组,方法2
       fmt.Println(a,s,b) //输出忽略
       
       // 定义一个3个元素的数组,最后一个元素是1,其它的都是用0初始化
       v := [...]int{2:1}
       fmt.Println(v) // 输出:[0 0 1]
    
       //根据索引值初始化数组
       e := [5]int{1:22,3:33,0:11}
       fmt.Println(e) //输出: [11 22 0 33 0]
       s := [2]string{0:"呵呵",1:"你好"}
       fmt.Println(s[0]) //输出: 呵呵
    
       //定义多维数组,数组的类型为数组
       //多维数组的定义,第一个为长度:代表有几行数组,可以使用[...],第二个为宽:代表一行元素的个数
       a1 := [2][3]int{
          {1,2,3},
          {4,5,6},
       }
       fmt.Println(a1[0][1]) //输出 2
    }
    

    切片

    切片底层使用的数组
    数组和切片的区别:

    1. 容量是否可伸缩

    2. 是否可以比较

    切片基础用法

    package main
    
    import (
       "fmt"
       "sort"
    )
    
    func main(){
       var nilSlice []int // 空切片,等于nil,长度为0
       slice := []int{}   // 空切片,不等于nil,长度为0
       fmt.Println(nilSlice == nil) //输出 true
       fmt.Println(len(slice))      //输出 0
    
       //基于数组得到切片
       a := [5]int{1,2,3,4,5}
       b := a[1:4]
       c := b[:]
       fmt.Println(b,c)
       // make函数构造切片,初始化数据,如果切片没有初始化,默认等于nil
       d := make([]int, 5, 10) //1类型,2切片的长度,3切片的容量(不写默认为长度)
       fmt.Println(d)
       // append函数,可以动态增加切片的容量,重新申请内存地址
       sum := []int{1,2,3}
       e := append(sum, b...) //append函数可以同时增加多个元素,append(sum, a, b)
       fmt.Println(e)
       //len函数查看长度,cap函数查看容量
       a1 := []int{1,2,3,4,5}
       a2 := make([]int, 5)
       a3 := a2     //这样的写法,内存地址相等
       copy(a2, a1) //把a1的内容拷贝到a2里,a1和a2虽然内容一样,但不在一个内存地址
       a2[0] = 100
       fmt.Println(a1) //输出:[1 2 3 4 5]
       fmt.Println(a2) //输出:[100 2 3 4 5]
       fmt.Println(a3) //输出:[100 2 3 4 5]
       //sort排序 方法有Ints,Strings等
       s := [...]int{10,22,8,2}
       sort.Ints(s[:])
       fmt.Println(s)  //输出:[2 8 10 22]
    }
    

    结构体切片

    // struct结构体切片用法
    m := []struct{
       i int
       b bool
    }{}
    m = append(m, struct {
       i int
       b bool
    }{i: 1, b: true})
    fmt.Println(m)       // 输出:[{1 true}]
    
    // 自定义结构体切片用法
    type mylist struct {
       l int
       name bool
    }
    
    // 使用双花括号{{}}可以为自定义结构体切片赋值
    l := []mylist{{100, false}}
    fmt.Println(l)      // 输出:[{100 false}]
    
    // 动态扩容结构体切片
    m1 := []mylist{}
    m1 = append(m1, mylist{10, false})
    fmt.Println(m1[0])  // 输出:{10 false}
    

    map

    map基础用法

    map是无序的,kv类型。
    map的key不能为函数值。

    func main(){
       //声明map类型,但是没有初始化,表示还没有申请内存,d的初始值就是nil
       var d map[string]int //k是string类型,v是int类型
       fmt.Println(d == nil)
       // map初始化,表示申请内存了
       d = make(map[string]int, 5)
       // 增加键值对
       d["age"] = 18
       fmt.Println(d["age"])
       
       // 声明map同时完成初始化
       // 方法1
       score1 := map[string]int{"wsy":1,"old":2}
       // 方法2
       a := map[int]bool{
          1: true,
          2: false,
       }
       // 删除键值对
       delete(a, 2)
       // 判断key存不存在
       var score = make(map[string]int, 5)
       score["cby"] = 1
       if v, ok := score["boy"]; ok{
          fmt.Println(v)
       }else{
          fmt.Println("这个key不存在")
       }
    }
    

    map key 排序

    // map的key是无序的,有些时候需要有序
    // 思路:遍历map把key存到切片里,排序切片里的key,然后遍历切片输出map[k]
    func SortKey() {
    	d := map[int]string{
    		0: "a",
    		1: "b",
    		2: "c",
    		3: "d",
    	}
    	var n []int
    	for k, _ := range d {
    		n = append(n, k)
    	}
    	sort.Ints(n)
    	for _, i := range n {
    		fmt.Printf("%v
    ", d[i])
    	}
    }
    

    map实现值为结构体的使用方法

    // 把map的值定义为结构体,并增加默认的字段
    dict := make(map[string]struct{
       name string
        age int
    })
    type mystery struct {
       name string
       age int
    }
    dict["a"] = mystery{
       name: "cby",
       age: 16,
    }
    fmt.Println(dict["a"]) //输出:{cby 16}
    

    map实现集合的方式

    mySet := map[int]bool{} //key可以是任意类型,不能是动态可变的
    mySet[1] = true
    n := 3
    if mySet[n]{
        fmt.Println("存在",n)   
    }else{
        fmt.Println("不存在",n)
    }
    

    map创建工厂

    // v可以为函数
    //m := make(map[int]func(op int) int, 5)
    m := map[int]func(op int) int{}
    m[1] = func(op int) int { return op }
    m[2] = func(op int) int { return op * op }
    m[1](10)
    m[2](10) //调用函数
    

    指针

    指针是带类型的,如*int, *string
    &取地址,*取值

    func main() {
        // var a *int //a是一个int类型的指针
        // var c *[3]string
        // 以上是错误的写法
        var a = new(int) //得到一个int类型的指针
        *a = 10
        fmt.Println(*a)
        // 初始化数组类型的指针
        var c = new([3]int) //得到一个数组类型的指针 
        fmt.Println(c)
        c[0] = 1           //或者 (*c)[0] = 1
        fmt.Println(*c)
    }
    

    指针数组

    a, b := 1, 2
    pointArr := [...]*int{&a, &b}
    fmt.Println("指针数组:", pointArr) // [0xc0... 0xc0...]
    

    数组指针

    arr := [...]int{1, 2, 3}
    arrPoint := &arr
    fmt.Println("数组指针:", arrPoint) // &[1 2 3]
    

    make和new的区别

    值类型:int, float, bool, string, array(数组), struct(结构体)
    引用类型:map, slice(切片), func(函数), pointer(指针), channel

    • new:是用来初始化值类型的指针的,并返回指针!返回的指针不是nil和空指针,指向了新分配的内存空间,里面存储的是零值。
    • make:是用来初始化引用类型的,返回初始化后的非零值。

    函数

    函数的形参:

    • 值类型的参数,参数是实参的拷贝,修改参数不会影响实参。
    • 引用类型的参数,参数是实参的引用,修改参数会间接修改实参。

    普通函数

    //参数类型简写,相同类型的参数用逗号分隔宫格后边接类型
    func f1(name1, name2 string){
       fmt.Println(name1)
       fmt.Println(name2)
    }
    
    //可变参数 0个或多个, 参数被看作该类型的切片
    func sum(vals ...int) []int {
    	total := 0
    	for _, v := range vals {
    		total += v
    	}
    	vals = append(vals, total)
    	return vals
    }
    //调用上边的函数
    l := []int{1, 2, 3}
    sum(l...)
    
    //无返回值
    func f3(){
       fmt.Println("没有返回值")
    }
    
    //有返回值
    func f4(a, b int) int{
       return a+b
    }
    
    //(闭包)返回带返回值的函数
    func f5() func() int {
       var x int
       return func() int {
           return x * x
       }
    }
    
    //多返回值,必须用括号括起来,用英文逗号分隔
    func f6(a, b int) (int, int){
       return a+b, a-b
    }
    
    //命名的返回值,直接return就行
    func f7(a, b int) (sum int, sub int){
       sum = a + b
       sub = a - b
       return
    }
    

    匿名函数

    //方法1
    var MyFunc = func (sum int){
       fmt.Println(sum)
    }
    
    func main(){
       MyFunc(10)
        
       // 方法2,匿名函数后边直接+括号调用
       func (sum int){
          fmt.Println(sum)
       }(10)
    }
    

    闭包

    闭包: 函数调用了它外层的变量。
    这个函数返回值是个函数。

    func closure(key string) func(name string){
       return func(name string){
          fmt.Println(name)
          fmt.Println(key)
       }
    }
    
    func main(){
       // 闭包
       f1 := closure("呵呵")// 得到闭包函数,接收closure函数返回值返回的函数
       f1("cby")//调用闭包函数
    
       f2 := closure("哈哈")// 得到闭包函数
       f2("wsy")//调用闭包函数
    }
    

    异常处理

    基本使用

    • recover() 必须搭配defer使用
    • defer一定要在可能引发panic的语句之前定义
    package main
    import "fmt"
    
    func f1() {
       // 定义个defer匿名函数用来捕获panic引发的异常
       defer func() {
          // recover
          err := recover() //尝试将函数从当前的异常状态恢复过来
          fmt.Println("recover抓到了panic异常", err)
       }()
       var a []int
       a[0] = 100 //panic报错,因为切片没有被初始化
       fmt.Println("f1") //不执行
    }
    
    func f2() {
       panic("f2 测试个错误") //主动抛出异常,结束代码
       fmt.Println("f2")
    }
    
    // panic错误
    func main() {
       f1()
       f2()
       fmt.Println("这是main函数")
    }
    

    定义捕获异常函数

    func GoRecover() {
    	if err := recover(); err != nil {
    		return
    	}
    }
    // 使用方法
    func demo() {
       defer GoRecover()
       // pass
    }
    

    包的导入和初始化函数init

    package main
    // 如果想把自己的go文件当做包导入,函数名、结构体、字段名、变量名的第一个字母必须大写,否则导入包时找不到数据
    import (
       "fmt"
       // 包的路径是src目录下边的路径
       m "Go学习/day5/demo1" // 给导入包做别名,使用方法 m.Add(10)
       // 如果只导入,不适应包内部的数据时,可以使用匿名导入包
       _ "包路径" // 下划线代表 匿名导入包
    )
    
    // init的执行顺序,先执行全局声明,再执行初始化操作,最后执行代码
    func init(){
       fmt.Println("包被初始化了")
    }
    
    func main()  {
       fmt.Println("执行代码")
    }
    

    结构体

    结构体技巧

    结构体可以用于map的key和value类型

    type m struct {
    	age    int
    	name string
    }
    
    func main() {
    	h := make(map[m]int) // 把m结构体用于map的key类型
    	h[m{1,"a1"}] = 1
    	fmt.Println(h[m{1,"a1"}]) // out:1
    	fmt.Println(h[m{5,"a1"}]) // out:0
    }
    

    方法

    构造函数

    继承,结构体嵌套

    接口

    空接口 interface{}

    任何其他类型都实现了空接口,空接口可以 赋任何类型的值。类似python的object对象。
    任意变量都可以存到空接口变量中。

    ok语法断言接口类型

    v, ok := a.(int)
    if !ok {
         fmt.Println(“…”)
    }
    fmt.Println(v)
    

    类型断言

    package main
    import (
       "fmt"
    )
    // 类型断言
    func iFok(a interface{})  {
       switch n := a.(type) {
       case int:
          fmt.Println("整数", n)
       case string:
          fmt.Println("字符串", n)
    default:
    fmt.Println(“…”)
       }
    }
    
    // 定义一个值可以存放任何类型的map
    func main()  {
       opmap := make(map[string]interface{})
       opmap["姓名"] = "崔百一"
       opmap["年龄"] = 26
       opmap["布尔值"] = true
       opmap["浮点"] = 1.20
       fmt.Println(opmap)
    
       iFok(opmap["年龄"])
       iFok(opmap["姓名"])
    }
    

    反射

    package main
    
    import (
    	"fmt"
    	"reflect"
    )
    
    // 通过反射修改结构体
    type Student struct {
    	Name string `json:"name" db:"my"`
    	Age int `json:"age" db:"3306"` 
    }
    
    func main() {
    	// 通过反射获取指针类型,并修改值
    	var x float32 3.5
    	demo(&x)
    	fmt.Println(x)
    
    	// 通过反射修改结构体
    	var s Student
    	v := reflect.ValueOf(&s)
    	// Elem()获取指针指向的变量,相当于*s
    	v.Elem().Field(0).SetString("cby")     // Field(下标)获取结构体下标字段的值
    	v.Elem().FieldByName("Age").SetInt(28) // FieldByName()获取结构体字段的值
    	fmt.Printf("%#v
    ", s)
    
    	// 获取结构体中tag信息
    	s1 := Struct{}
    	stag := reflect.TypeOf(s1)
    	field := stag.Field(0)     // 如果是结构体指针,则使用stag.Elem().Field(0)
    	fmt.Println(field.Tag.Get("json"), field.Tag.Get("db")) 
    }
    
    func demo(x Interface{}) {
    	v := reflect.ValueOf(x) // 获取值
    	// v := v.Type()        // 获取类型,和reflect.TypeOf功能是一样的
    	k := v.Kind()
    	switch k {
    	case reflect.Int64:
    		fmt.Printf("x is int64 is:%d", v.Int())
    	case reflect.Struct:
    		fmt.Println("结构体")
    		for i:=0;i<v.NumField();i++ {
    			field := v.Field(i)
    			fmt.Printf("name:%s,type:%v,value:%v
    ", field.Name, field.Type().Kind(), field.Interface())
    		}
    	case reflect.Ptr:
    		fmt.Println("指针")
    		v.Elem().SetFloat(6.5)
    	default:
    		ftm.Println("...")
    	}
    }
    

    并发编程

    goroutine

    package main
    
    import (
       "fmt"
       "sync"
    )
    // sync.WaitGroup 优雅的等待
    var wg sync.WaitGroup
    
    func hello()  {
       defer wg.Done() //计数器-1
       fmt.Println("Hello")
    }
    
    func main()  {
       wg.Add(2)  //计数器+2
       go hello()
       go hello()
       fmt.Println("世界")
       wg.Wait() //阻塞,一直等待所有goroutine结束
    }
    

    chan

    并发原语和chan的应用场景

    1. 共享资源的并发访问使用传统并发原语;
    2. 复杂的任务编排和消息传递使用 Channel
    3. 消息通知机制使用 Channel,除非只想 signal 一个 goroutine,才使用 Cond
    4. 简单等待所有任务的完成用 WaitGroup,也有 Channel的推崇者用 Channel,都可以;
    5. 需要和 Select 语句结合,使用 Channel
    6. 需要和超时配合时,使用 ChannelContext

    创建chan

    并发:同一时间段同时在做多个事情
    并行:同一时刻同时在做多个事情

    c := make(chan int, 10)
    

    有缓存是异步的,无缓存是同步阻塞的
    这里有个缓冲,因此放入数据的操作c<- 0先于取数据操作 <-c
    由于c是无缓冲的channel,因此必须保证取操作<-c 先于放操作c<- 0
    取数据: <-c
    放数据: c <- 1
    有缓冲的channel,因此要注意“放”先于“取”
    无缓冲的channel,因此要注意“取”先于“放”

    接收chan

    x, b := <-ch // 第一个值是返回chan中的元素,第二个值是bool类型(如果为false,chan已经被close而且chan中没有缓存的数据,此时第一个值是零值)
    

    零值chan

    未初始化的chan的零值是nil,是一种特殊的chan,对值是nil的chan的发送接收调用者总是会阻塞的。

    清空chan

    for range ch {
    }
    

    单向通道

    func f0(ch chan int){...}   // 既能接收也能发送
    func f1(ch chan<- int){...} // 只能发送,往通道发送值
    func f2(ch <-chan int){...} // 只能接收,从通道取值
    

    关闭通道

    如果 chan 为 nil,close 会 panic;如果 chan 已经 closed,再次 close 也会 panic。
    只要一个 chan 还有未读的数据,即使把它 close 掉,你还是可以继续把这些未读的数据消费完,之后才是读取零值数据。

    方法1,推荐的写法

    func a(ch chan int) {
        for i :=0; i<10; i++ {
              ch <- i
        }
        close(ch)  // 关闭管道
    }
    
    func main() {
        ch := make(chan int)
        go a(ch)
        for v := range ch {
             fmt.Println("data:", v)
        }
    }
    

    方法2

    v, ok := <-ch // ok是false,v为chan类型的零值
    if ok == false {...}  
    

    chan常见错误

    panic错误:

    1. close为nil的chan
    2. send已经close的chan
    3. close已经close的chan

    同步原语

    同步原语的适应场景

    • 共享资源。并发地读写共享资源,会出现数据竞争(data race)的问题,所以需要 Mutex、RWMutex 这样的并发原语来保护。
    • 任务编排。需要 goroutine 按照一定的规律执行,而 goroutine 之间有相互等待或者依赖的顺序关系,我们常常使用 WaitGroup 或者 Channel 来实现。
    • 消息传递。信息交流以及不同的 goroutine 之间的线程安全的数据交流,常常使用 Channel 来实现。

    检测并发访问共享资源的工具

    go run -race counter.go #如果有问题,会输出警告信息
    #开启了 race 的程序部署在线上,还是比较影响性能的。
    
    go tool compile -race -S counter.go
    #增加了 runtime.racefuncenter、runtime.raceread、runtime.racewrite、runtime.racefuncexit 等检测 data race 的方法。通过这些指令就能检测出data race问题。
    

    Mutex(互斥锁)

    适应场景:比如多个 goroutine 并发更新同一个资源,像计数器;同时更新用户的账户信息;秒杀系统;往同一个 buffer 中并发写入数据等等。如果没有互斥控制,就会出现一些异常情况,比如计数器的计数不准确、用户的账户可能出现透支、秒杀系统出现超卖、buffer 中的数据混乱等。

    var lock sync.Mutex // 声明锁
    lock.Lock()         // 加锁
    lock.Unlock()       // 解锁
    

    Mutex的其它用法

    1.很多情况下,Mutex 会嵌入到其它 struct 中使用:在初始化嵌入的 struct 时,也不必初始化这个 Mutex 字段,不会因为没有初始化出现空指针或者是无法获取到锁的情况。

    type Counter struct { 
        mu sync.Mutex                 
        Count uint64
    }
    

    2.采用匿名字段的方式:

    func main() {
        var counter Counter
        var wg sync.WaitGroup
        wg.Add(10)
        for i := 0; i < 10; i++ {
            go func() {
                defer wg.Done()
                for j := 0; j < 100000; j++ {
                    counter.Lock()
                    counter.Count++
                    counter.Unlock()
                }
            }()
        }
        wg.Wait()
        fmt.Println(counter.Count)
    }
    
    type Counter struct {
        sync.Mutex
        Count uint64
    }
    

    3.如果嵌入的 struct 有多个字段,我们一般会把 Mutex 放在要控制的字段上面,然后使用空格把字段分隔开来。即使你不这样做,代码也可以正常编译,只不过,用这种风格去写的话,逻辑会更清晰,也更易于维护。

    // 线程安全的计数器类型
    type Counter struct {
        CounterType int
        Name        string
    
        mu    sync.Mutex
        count uint64
    }
    
    // 加1的方法,内部使用互斥锁保护
    func (c *Counter) Incr() {
        c.mu.Lock()
        c.count++
        c.mu.Unlock()
    }
    

    4.实现一个线程安全的队列(通过slice实现的队列不是线程安全的,需要配合Mutex)

    type SliceQueue struct {
        data []interface{}
        mu   sync.Mutex
    }
    
    func NewSliceQueue(n int) (q *SliceQueue) {
        return &SliceQueue{data: make([]interface{}, 0, n)}
    }
    
    // Enqueue 把值放在队尾
    func (q *SliceQueue) Enqueue(v interface{}) {
        q.mu.Lock()
        q.data = append(q.data, v)
        q.mu.Unlock()
    }
    
    // Dequeue 移去队头并返回
    func (q *SliceQueue) Dequeue() interface{} {
        q.mu.Lock()
        if len(q.data) == 0 {
            q.mu.Unlock()
            return nil
        }
        v := q.data[0]
        q.data = q.data[1:]
        q.mu.Unlock()
        return v
    }
    

    RWMutex(读写锁)

    只有在读的操作多于写的操作时,使用读写锁可以提高性能。

    var rwlock sync.RWMutex
    rwlock.Lock()    // 加写锁
    rwlock.Unlock()  // 解写锁
    rwlock.RLock()   // 加读锁
    rwlock.RUnlock() // 解读锁
    rwlock.RLocker() //
    

    并发中使用map

    Go语言中内置的map不是并发安全的,需使用sync.MAP
    需要为map加锁来保证并发的安全性了,Go语言的sync包中提供了一个开箱即用的并发安全版map–sync.Map。开箱即用表示不用像内置的map一样使用make函数初始化就能直接使用。同时sync.Map内置了诸如Store、Load、LoadOrStore、Delete、Range等操作方法。

    var m = sync.Map{}
    m.Store(key, n)  // 给map增加key和value,Store设置值,Load加载值,Range遍历map
    

    atomic包

    Context

    作用

    1. 取消goroute任务
    2. 进行超时控制
    3. 传递通用参数
    import (
    	"context"
    	"time"
    )
    
    ctx, cancel := context.WithCancel(context.Background())
    // 1. 返回一个cancel函数,调用cancel函数的时候,会触发context.Done函数
    // 2. 当执行一个后台任务时,怎么取消这个后台任务?
    
    ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)  // 绝对时间  
    context.WithDeadline // 相对时间
    // 0. 通常用于数据库或者网络连接的超时控制
    // 1. 超过指定时间之后,会触发context.Done函数
    // 2. 当执行一个函数调用,特别是rpc调用时,怎么做超时控制?
    
    // 传递上下文通用参数
    context.WithValue(context.Background(), key, value) // 把参数设置到context中
    // context.Value(key) // 获取参数
    // 一个请求需要访问N个子系统,这时候如何跟踪各个子系统执行的情况呢?
    
    func demo(ctx context.Context) {
    	for {
    		select {
    		case <- ctx.Done():
    			return 
    		}
    	}
    }
    
    func main() {
    	ctx, cancel := context.WithCancel(context.Background())
    	defer cancel()
    }
    

    接口定义

    context.Context是一个接口,该接口定义了四个需要实现的方法。具体签名如下

    type Context interface {
        Deadline() (deadline time.Time, ok bool)
        Done() <-chan struct{}
        Err() error
        Value(key interface{}) interface{}
    }
    
    • Deadline方法需要返回当前Context被取消的时间,也就是完成工作的截止时间(deadline);

    • Done方法需要返回一个Channel,这个Channel会在当前工作完成或者上下文被取消之后关闭,多次调用Done方法会返回同一个Channel

    • Err方法会返回当前Context结束的原因,它只会在

      返回的Channel被关闭时才会返回非空的值;

      • 如果当前Context被取消就会返回Canceled错误;
      • 如果当前Context超时就会返回DeadlineExceeded错误;
    • Value方法会从Context中返回键对应的值,对于同一个上下文来说,多次调用Value 并传入相同的Key会返回相同的结果,该方法仅用于传递跨API和进程间跟请求域的数据;

    常用库

    sort

    n := []int{3,1,5,2}
    s := []string{"c","a","b"}
    x := {5.2, -1.3, 0.7, -3.8, 2.6}
    sort.Ints(n) // 排序int类型切片
    sort.Sort(sort.Reverse(sort.IntSlice(n))) // 倒序排序int类型切片
    sort.Strings(s) // 排序string类型切片
    sort.Sort(sort.Reverse(sort.StringSlice(s))) // 倒序排序string类型切片
    sort.Float64s(x) // 排序浮点数切片
    

    http

    import (
        "fmt"
    	"io/ioutil"
    	"net/http"
    	"crypto/tls"
    )
    func test() {
        // https请求跳过证书验证
    	tr := &http.Transport{
    		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
    	}
    	client := &http.Client{Transport: tr}
        resp, err := client.Get("https://www.baidu.com/test")
        // resp.StatusCode 可以获取响应状态码
    	if err != nil {
    		fmt.Printf("get api failed, err:%v
    ", err)
    	}
    	defer resp.Body.Close()
    	b, err := ioutil.ReadAll(resp.Body)
    }
    

    POST请求

    JsonData, err := json.Marshal(data) // data换成请求报文数据
    if err != nil {
    	fmt.Printf("序列化json失败, err:%v
    ", err)
    }
    response, err := http.Post("http://www.api.com", "application/json", strings.NewReader(string(JsonData)))
    if err != nil {
    	fmt.Printf("请求接口失败,err:%v
    ", err)
    }
    defer response.Body.Close()
    resp, err := ioutil.ReadAll(response.Body)
    if err != nil {
    	fmt.Printf("get response data failed, err:%v
    ", err)
    }
    fmt.Printf("请求接口成功response:%s
    ", string(resp))
    

    delete 和 put 请求

    res, err := http.NewRequest("DELETE", "http://www.api.com", nil) // 第三个参数如果有请求报文就换成请求报文
    if err != nil {
        fmt.Printf("请求失败, err:%v
    ", err)
    }
    // 添加header
    res.Header.Add("content-type", "application/json")
    response, _ := http.DefaultClient.Do(res)
    defer response.Body.Close()
    data, _ := ioutil.ReadAll(response.Body)
    fmt.Printf("删除成功response:%s
    ", string(data))
    

    time

    package main
    
    import (
       "fmt"
       "time"
    )
    // unix时间戳转换成时间
    func mytime(timestamp int64) {
       timeObj := time.Unix(timestamp, 0) //将时间戳转为时间格式
       year := timeObj.Year()     //年
       month := timeObj.Month()   //月
       day := timeObj.Day()       //日
       hour := timeObj.Hour()     //小时
       minute := timeObj.Minute() //分钟
       second := timeObj.Second() //秒
       fmt.Printf("%4d-%02d-%02d %02d:%02d:%02d
    ", year, month, day, hour, minute, second)
    }
    
    // 将字符串时间转换为时间戳
    func main() {
        location, _ := time.LoadLocation("Asia/Shanghai")
        TIME_LAYOUT := "2006-01-02 15:04"
        t, err := time.ParseInLocation(TIME_LAYOUT, "2020-03-20 11:11", location)
        if err != nil {
            fmt.Println(err)
        }
        fmt.Println(t.Unix())
    }
    
    // 时间格式化
    func formattime(){
       now := time.Now()
       fmt.Println(now.Format("2006-01-02-15-04"))
       fmt.Println(now.Format("2006/01/02 15:04:05.000")) //05表示秒,000表示纳秒, 加上PM表示上下午
       fmt.Println(now.Format("15:04 2006-01-02"))
       fmt.Println(now.Format("01-02-15"))
    }
    // 获取2分钟之前的时间
    t := now.Add(time.Second * -120)
    // 时间格式化字符串
    strconv.FormatInt(time.Now().Unix(), 10)
    // 定义一个5秒执行一次的定时任务,Second秒,Minute分,Hour时
    func tickDemo() {
       ticker := time.Tick(time.Second * 5) //定义一个5秒间隔的定时器
       for range ticker {
          go func() {
               fmt.Println("hello")
          }()
       }
    }
    
    func main()  {
       now := time.Now().Unix()
       mytime(now)
       formattime()
       tickDemo()
    }
    

    json

    // 解析第三方的api数据格式技巧
    s := `{
    "data": [
        {
            "synonym": "go",
            "tag":"type1",
            "name": "cby"
        },
        {
            "synonym": "python",
            "tag":"type2",
            "name": "cby"
        }
    ]
    }`
    
    // 方法1
    m := make(map[string]interface{})
    json.Unmarshal([]byte(s), &m)
    fmt.Printf("%+v
    ", m["data"].([]interface{})[1]).(map[string]interface{})["synonym"] // 得有类型声明,告诉是什么类型
    
    // 方法2(推荐),用结构体来解析想要获取的json数据值
    m := struct {
        Data []struct {
            Synonym string `json:"synony"`
            Tag string `json:"tag"`
        } `json:"data"`
    }{}
    json.Unmarshal([]byte(s), &m)
    fmt.Printf("%+v, %+v
    ", m.Data[1].Synonym, m.Data[1].Tag)
    

    文件读操作

    package main
    
    import (
        "bufio"
        "fmt"
        "io"
        "os"
    )
    
    func main() {
        f, err := os.Open("input.dat")
        if err != nil {
            fmt.Println("err")
            return 
        }
        defer f.Close()
    
        inputReader := bufio.NewReader(f) // 获取一个读取器变量
        for {
            // 带缓冲的
            // buf := make([]byte, 1024)
            // n, err := inputReader.Read(buf)
            // if (n == 0) { break}
            inputString, readerError := inputReader.ReadString('
    ')
            fmt.Printf("The input was: %s", inputString)
            if readerError == io.EOF {
                return
            }      
        }
    }
    

    文件写操作

    /* 
    os.O_WRONLY 只写 
    os.O_CREATE 创建文件 
    os.O_RDONLY 只读 
    os.O_RDWR 读写 
    os.O_TRUNC 清空 
    os.O_APPEND 追加 
    */ 
    
    func demo1() {
    	file, err := os.OpenFile("xx.txt", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
    	if err != nil {
    		fmt.Println("open file failed, err:", err)
    		return
    	}
    	defer file.Close()
    	str := "hello 沙河"
    	file.Write([]byte(str))       //写入字节切片数据
    	file.WriteString("hello")     //直接写入字符串数据
    }
    
    func demo2() {
    	file, err := os.OpenFile("xx.txt", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
    	if err != nil {
    		fmt.Println("open file failed, err:", err)
    		return
    	}
    	defer file.Close()
    	writer := bufio.NewWriter(file)
    	for i := 0; i < 10; i++ {
    		writer.WriteString("hello
    ") //将数据先写入缓存
    	}
    	writer.Flush() //将缓存中的内容写入文件
    }
    

    正则表达式

    package main
    
    import (
        "fmt"
        "regexp"
    )
    
    var text = `my0 123@qq.com
                my1 abc@qq.com
                my2 qwe@163.com
    `
    
    func main() {
        r := regexp.MustCompile(`([a-zA-Z0-9]+)@([a-zA-Z0-9]+)(.[a-zA-Z0-9.]+)`)
        // FindAllStringSubmatch 方法获取正则匹配括号里的内容,返回二维切片[[x xx] [1 2]]
        // 参数1: 需处理的数据, 参数2: -1表示获取全部匹配的字符串
        data := r.FindAllStringSubmatch(text, -1)
        // FindAllString 方法获取正则表达式匹配到的所有内容,返回string
        // data := r.FindAllString(text, -1)
        for _, d := range data {
            fmt.Println(d[1])
        }
    }
    

    gorm

    package main
    
    import (
    	// "fmt"
    
    	"github.com/jinzhu/gorm"
    	_ "github.com/jinzhu/gorm/dialects/mysql"
    )
    
    // 数据库表结构字段
    type User struct {
    	Name string
    	Gender string
    	Age int
    }
    
    func main() {
      db, err := gorm.Open("mysql", "cby:cby1234@(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local")
      if err!= nil{
          panic(err)
      }
      defer db.Close()
      db.AutoMigrate(&User{})
      u := User{"cby", "nam", 28}
      // 创建记录
      db.Create(&u)
      // 执行原生sql,增,删,改
      db.Exec("delete from test.users where name = 'name';")
    }
    

    filepath

    Glob类似python的glob

    package main
    
    import (
       "fmt"
       "path/filepath"
    )
    
    func main()  {
       f := "/Users/cby/Desktop/Go学习/*/*.go"
       s, err := filepath.Glob(f)
       if err != nil{
          return
       }
       for i := range s{
          fmt.Println(s[i])
       }
    }
    
    你好
  • 相关阅读:
    Beginning ARC in iOS 5 Tutorial Part 1【转】
    移除所有子 View 时不使用循环代码的一种方式
    FrankCucumber Core Frank Steps
    iPhone开发重构:从硬编码到模型到规律【转】
    iPhone开发重构:提取类以构成模板模式 【转】
    FrankCucumber Instance Method Summary
    iOS自动化测试(原创)
    FrankCucumber Step Definition compendium (转载)
    iPhone开发重构:提供辅助创建方法以简化实例创建【转】
    App Store 审核指南[转]
  • 原文地址:https://www.cnblogs.com/cuibaiyi/p/15450339.html
Copyright © 2020-2023  润新知