go语言学习笔记

Go学习

一直想学一门新语言,难度又不想太大,C和Java都会但是不怎么精通,某天看到Go语言,好的,就是它了。总体来说,go语言的学习还是相对简单,有编程基础的入手很快。

简介

go是一种并发、带垃圾回收、快速编译,静态类型、编译型、强类型的语言。主要用途有高并发服务器、区块链、云计算等。

正文

  • 一般Go语言推荐使用 驼峰式 命名
  • 安装(windows系统)系统变量设置 GOROOT 是go的安装目录,并且添加到path中
  • 在 Go 语言中没有类(Class)的概念
  • 查看go版本 go version
  • 运行go文件 go run hello.go
  • 结尾不要分号,除非一行上有多条语句
  • 变量声明的顺序,不影响执行的
  • 常量声明使用 const
  • Go的垃圾回收机制会回收不被使用的内存,但是这不包括操作系统层面的资源,比如打开的文件、网络连接,因此我们必须显式的释放这些资源

赋值

  • 正常赋值

    var a int = 1
    var b = 1
    
  • 简短变量声明,可用于声明和初始化局部变量, 以 a := 1形式声明变量,
    变量类型根据表达式来自动推导,如果已经使用 :=了,变量第二次使用就是赋值
    只能在函数内部使用

    a := 1
    
  • 元组赋值允许同时更新多个变量的值。在赋值之前,赋值语句右边的所有表达式将会先进行求值,
    然后再统一更新左边对应变量的值。

    // 交换值
    c, d := 2, 3
    c, d = d, c
    fmt.Println(c)
    fmt.Println(d)
    
  • 匿名变量不占用命名空间,不会分配内存,所以匿名变量之间不存在重复声明

    for a, _ := range(strs)
    
  • 注意 ,如下 b是a的拷贝,地址是不一样的

    a := [3]int{1, 2, 3}
    b := a
    b[0] = 999
    

变量的声明周期

对于在包一级声明的变量来说,它们的生命周期和整个程序的运行周期是一致的。
局部变量的生命周期则是动态的:每次从创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收。

那么自动垃圾收集器一个变量是何时可以被回收的呢?从每个包级的变量和每个当前运行函数的每一个局部变量开始,通过指针或引用的访问路径遍历,是否可以找到该变量。
如果不存在这样访问路径那么该变量是不可达的,也就是说它是否存在并不会影响程序后续的计算结果。因为一个变量的有效周期只取决于是否可达,因此一个循环迭代内部的局部变量的生命周期可能超出其局部作用域。
同时,局部变量可能在函数返回之后依然存在。

关于包

  • main 包比较特殊,它定义了一个独立可执行的程序,而不是一个库。
  • 在 main包 里的 main 函数也很特殊,它是整个程序执行时的入口
  • 缺少了包或者导入多余的包,程序都无法编译通过
  • 包中的变量,大写的就是导出的
  • 我们可以用一个特殊的init初始化函数来简化初始化工作,不能被调用或引用外其他行为和普通函数类似,可以多个,按照顺序调用
  • 导入同名包,重命名
    import ("crypto/rand"mrand "math/rand" // alternative name mrand avoids conflict
    )
    

基础数据类型

  • int8、int16、int32、int64 和默认的 intuint
  • 哪怕int是32位的s环境,intint32 是不同的数据类型
  • 无符号的整数类型 uintptr,没有指定具体的bit大小但是足以容纳指针
  • 其中有符号整数采用2的补码形式表示,也就是最高位用来表示符号位。例如,int8类型的值域是 -128到127 而uint8类型整数的是 0到255
  • 取模运算符,只能是整数,小数不行
  • 除法 / 的结果依赖于操作数是否全为整数,5.0/4.0 = 1.25 5/4 = 1
  • 一个算术运算的结果,不管是有符号或者无符号的,如果需要更多的bit位才能正确表示的话,就说明计算结果是溢出了,超出的高位会丢弃。
    // byte 和 int8等价的
    var i1 int8 = 127
    var b1 byte = 123
    var i2 int16 = 244
    var i3 int32 = 244
    var i4 int64 = 244
    fmt.Println(i1)
    fmt.Println(i3)
    fmt.Println(i2)
    fmt.Println(i4)
    var n1 uint8 = 255
    fmt.Println(n1)
    fmt.Println(b1)var n2 int = 333
    var n3 uint  = 222
    fmt.Println(n2)
    fmt.Println(n3)
  • 位运算
    // &   位运算 AND
    // |   位运算 OR
    // ^   位运算 XOR, 如果是一个数就是取反
    // &^  位清空或者按位置零(AND NOT)x &^ y,就是结果把y的位是1的清掉变成0
    // <<  左移
    // >>  右移
    // 最好用无符号运算运算左移和右移
    
  • 使用 uint做遍历会出现死循环,比如 var a unit = 11; a >=0; a--,那么 a>=永久成立,无符号数往往只有在位运算或其它特殊的运算场景才会使用,就像bit集合、分析二进制文件格式或者是哈希和加密操作等
  • 浮点数,一个float32类型可提供大约6个十进制数的精度,而float64则可以提供约15个十进制数的精度,通常应该优先使用float64类型
    const e = 2.71828
    for x := 0; x < 8; x++ {
    fmt.Printf("x = %d e^x = %8.3f\n", x, math.Exp(float64(x)))
    }
    var z float64
    fmt.Println(z, -z, 1/z, -1/z, z/z)//0 -0 +Inf -Inf NaN
    fmt.Println(math.IsNaN(z/z))
    
  • 复数也支持,暂时忽略,基本用不到
  • 布尔值 bool 和其它语言基本一样

Printf

fmt.Sprintf 方法是把格式化后的字符串返回

// 打印布尔值
fmt.Printf("%t\n", true) //输出结果为 true// 打印整数
fmt.Printf("%d\n", 123) //输出结果为 123// 打印二进制
fmt.Printf("%b\n", 33) //输出结果为 100001// 打印八进制
fmt.Printf("%o\n", 33) //输出结果为 41// 打印十六进制
fmt.Printf("%x\n", 33) //输出结果为 21// 打印字符
fmt.Printf("%c\n", 97) //输出结果为a// 字符串
fmt.Printf("%s\n", "halo") //输出结果为a// 打印指针
fmt.Printf("%p\n", &a) //输出结果为 0xc000114018// %v 
// %v的方式  = &{david 1}
// %+v的方式 = &{name:david id:1}
// %#v的方式 = &main.student{name:"david", id:1}

字符串

  • 字符串是一个不可改变的字节序列,通常认为是用UTF8编码的Unicode码点(rune)序列
  • 第i个字节并不一定是字符串的第i个字符,因为对于非ASCII字符的UTF8编码会要两个或多个字节
  • 子串切片 s[0:4] 索引可省略
  • 字符串拼接,就是加号
  • 可以比较,是字符串自然编码的顺序
  • 因为字符串是不可修改的,所以 s[0] = 'L' 是错误的
  • Go语言源文件总是用UTF8编码,并且Go语言的文本字符串也以UTF8编码的方式处理
  • 原生的字符串面值形式是...,使用反引号代替双引号。在原生的字符串面值中,没有转义操作。
  • Unicode码点对应Go语言中的rune整数类型(译注:rune是int32等价类型),如果一个符文序列表示为一个int32序列,肯定会浪费很多资源,后来UTF8编码是由Go语言之父发明现在已经是Unicode的标准。
  • Go语言的range循环在处理字符串的时候,会自动隐式解码UTF8字符串
func demo32()  {s := "halo bang bang"fmt.Println(len(s))     // "12"fmt.Println(s[0] == 'h')// fmt.Println(s[30])fmt.Println(s[0], s[7]) // "104 119" ('h' and 'w')// 字符串s的0-5的子串fmt.Println(s[0:4])fmt.Println(s[:4])fmt.Println(s[5:])s1 := "你好啊"fmt.Println(s1[0])fmt.Println(s + s1)// 原生的字符串面值中const GoUsage = `Go is a tool for managing Go source code.Usage:go command [arguments]...`println(GoUsage)
}// 判断是否是前缀
func HasPrefix(s, prefix string) bool {return len(s) >= len(prefix) && s[:len(prefix)] == prefix
}
// 判断是子串
func Contains(s, substr string) bool {for i := 0; i < len(s); i++ {if HasPrefix(s[i:], substr) {return true}}return false
}s := "Hallo, 世界"s1 := "你好"fmt.Println(s)fmt.Println(len(s))  // 13fmt.Println(len(s1)) // 6 utf8中每个中文有3位fmt.Println(utf8.RuneCountInString(s)) // "9"// 这样才可以完整的打印s中的每个字符for i := 0; i < len(s); {r, size := utf8.DecodeRuneInString(s[i:])fmt.Printf("%d\t%c\n", i, r)i += size}for i, r := range s {fmt.Printf("%d \t %q \t %d \n", i, r, r)}{// "program" in Japanese katakanas := "プログラム"fmt.Printf("% x \n", s) // "e3 83 97 e3 83 ad e3 82 b0 e3 83 a9 e3 83 a0"r := []rune(s)fmt.Printf("%x \n", r)  // "[30d7 30ed 30b0 30e9 30e0]"fmt.Println(string(r)) // "プログラム"fmt.Println(string(0x4eac)) // "京"}

strings库

str := "a/b/a/c.go"// 大小写转换
fmt.Println("strings.ToLower",strings.ToLower("ABHHH"))
fmt.Println("strings.ToUpper",strings.ToUpper(str))// 查找类型的操作
fmt.Println(strings.LastIndex(str, "/"))
fmt.Println(strings.HasPrefix(str, "a"))
fmt.Println(strings.HasSuffix(str, "go"))
fmt.Println(strings.Contains(str, "a/b"))
fmt.Println(strings.Index(str, "a"))
fmt.Println(strings.LastIndex(str, "a"))
fmt.Println(strings.Count("aaaaabb", "a")) // 有几个子串// 字符串操作
var strs = []string{"hh", "bb"}
fmt.Println(strings.Join(strs, "/"))//字符串数组的join方法
fmt.Println(strings.Fields("ffied"))
fmt.Println(strings.Repeat(str, 3))
// cut和split一样,cut只分两部分
fmt.Println(strings.Cut(str, "/b"))
fmt.Println(strings.Split(str, ""))
fmt.Println(strings.Replace(str, "a", "z", 1))
fmt.Println(strings.ReplaceAll(str, "a", "z"))
// Trim去开头或者结尾某个字符
fmt.Println(strings.Trim(str, "a"))
fmt.Println(strings.Trim(str, "go"))
// 去除前后的空格,不去中间的空格
fmt.Println(strings.TrimSpace(" aaaaabb "))func addEveryString(r rune) rune { return r + 1 }
// strings.Map对字符串中的每个字符调用addEveryString函数,并将每个addEveryString函数的返回值组成一个新的字符串返回给调用者
fmt.Println(strings.Map(addEveryString, "Admix"))    // "Benjy"
fmt.Println(strings.Map(addEveryString, "Hello"))    // "Ifmmp"
fmt.Println(strings.Map(addEveryString, "David"))    // "Ebwje"

方法 - 整数数组转字符串

func intToString(values []int) string {var buf bytes.Bufferbuf.WriteRune('[')for i, v := range values {if i > 0 {buf.WriteString(", ")}fmt.Fprintf(&buf, "%d", v)}buf.WriteRune(']')return buf.String()
}

字符串转数字

// 字符串转数字,第三位是16表示返回值是int16
var i, e = strconv.ParseInt("1f", 16, 16)
if e == nil {fmt.Println("parsed success, the result is ", i)
}else {fmt.Println("parsed failed")
}// 字符串转数字
var i1, e1 = strconv.Atoi("1f")
fmt.Println(i1)
fmt.Println(e1)

数字转字符串

// 数字转字符串
var str1 string = strconv.Itoa(111)
fmt.Println(str1)

常量

常量表达式的值在编译期计算,而不是在运行期,每种常量的潜在类型都是基础类型。可以同时声明多个常量。

const A11 = 11
const (A22 = 22A33 = 33
)

iota自动生成器,可以按照一定规则重复生成一些数据

type Weekday intconst (Sunday Weekday = iota // iota常量生成器初始化MondayTuesdayWednesdayThursdayFridaySaturday
)type Flags uintconst (FlagUp Flags = 1 << iota // 1FlagBroadcast            // 2FlagLoopback             // 4FlagPointToPoint         // 8FlagMulticast            // 16 
)

无类型常量,可以进行超出正常大小的计算,比如ZiB,可以有256bit的运算精度。
只有常量可以是无类型的。当一个无类型的常量被赋值给一个变量的时候,出现在有明确类型的变量声明的右边,无类型的常量将会被隐式转换为对应的类型,如果转换合法的话。

const (_ = 1 << (10 * iota)KiB // 1024MiB // 1048576GiB // 1073741824TiB // 1099511627776             (exceeds 1 << 32)PiB // 1125899906842624EiB // 1152921504606846976ZiB // 1180591620717411303424    (exceeds 1 << 64)YiB // 1208925819614629174706176
)

数组

数组是定长的,默认情况下,数组的每个元素都被初始化为元素类型对应的零值。

var a [3]int       // array of 3 integers
fmt.Println(a[0])        // print the first element
var strs = []string{"flower","flow","flight"}
// Print the indices and elements.
for i, v := range a {fmt.Printf("%d %d\n", i, v)
}

... 表示,数组的长度取决于初始化的值的数量

q := [...]int{1, 2, 3}

如果一个数组的元素类型是可以相互比较的,那么数组类型也是可以相互比较的

a := [...]int{1,2}
c := [...]int{1,2}
d := [...]int{1,3}
fmt.Println(a == c) // true
fmt.Println(a != d) // true

函数调用的时候,函数的每个调用参数将会被赋值给函数内部的参数变量,所以函数参数变量接收的是一个复制的副本。因为函数参数传递的机制导致传递大的数组类型将是低效的,并且对数组参数的任何的修改都是发生在复制的数组上,并不能直接修改调用时原始的数组变量。

数组依然是僵化的类型,我们一般使用slice来替代数组。

slice

  • slice的语法和数组很像,只是没有固定长度而已。
  • 一个slice由三个部分构成:指针、长度和容量。指针指向第一个slice元素对应的底层数组元素的地址,slice第一个元素并不一定是数组的第一个元素。长度对应slice中元素的数目;长度不能超过容量,容量一般是从slice的开始位置到底层数据的结尾位置。内置的len和cap函数分别返回slice的长度和容量。
  • 多个slice之间可以共享底层的数据,并且引用的数组部分区间可能重叠。
  • 切片可以再切片,超出cap(s)的上限将导致一个panic异常,但是超出len(s)则是意味着扩展了slice。
    f := []string{"1月", "2月", "3月", "4月", "5月", "6月"}
    q1 := f[:3]
    q2 := f[3:]
    fmt.Println(f)
    fmt.Println(q1)
    fmt.Println(q2)
    fmt.Println(q2[1:4])
    
  • 字符串也可以切片
a:= "healo"
fmt.Println(a[1:4])b:= []byte{'1', '2', '3'}
fmt.Println(b[:2])
  • 对切片的操作

    // 反转切片
    func reverse(s []int) {for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {s[i], s[j] = s[j], s[i]}
    }arr1 := [...]int{0, 1, 2, 3, 4, 5}
    reverse(arr1[:])
    fmt.Println(arr1) // "[5 4 3 2 1 0]"s := []int{0, 1, 2, 3, 4, 5}
    reverse(s[:2])
    reverse(s[2:])
    reverse(s)
    fmt.Println(s) // "[2 3 4 5 0 1]"
  • 和数组不同的是,slice之间不能比较。而且slice会隐式地创建一个合适大小的数组,然后slice的指针指向底层的数组。

  • 内置的make函数创建一个指定元素类型、长度和容量的slice。容量部分可以省略,在这种情况下,容量将等于长度

    make([]T, len)
    make([]T, len, cap) // same as make([]T, cap)[:len]
    
  • copy方法,第一个参数是要复制的目标slice,第二个参数是源slice ,copy函数将返回成功复制的元素的个数

    slice := []int{1,2,3,4}
    num :=copy(slice[1:], slice[2:])
    fmt.Println(num) // 2
    fmt.Println(slice) // [1 3 4 4]
    
  • 内置的append函数用于向slice追加元素,不能确认新的slice和原始的slice是否引用的是相同的底层数组空间,也不能确认在原先的slice上的操作是否会影响到新的slice。所以通常是将append返回的结果直接赋值给输入的slice变量。

func appendInt(x []int, y int) []int {var z []intzlen := len(x) + 1if zlen <= cap(x) {// There is room to grow.  Extend the slice.z = x[:zlen]} else {// There is insufficient space.  Allocate a new array.// Grow by doubling, for amortized linear complexity.zcap := zlenif zcap < 2*len(x) {zcap = 2 * len(x)}z = make([]int, zlen, zcap)// 为了提高内存使用效率,新分配的数组一般略大于保存x和y所需要的最低大小。通过在每次扩展数组时直接将长度翻倍从而避免了多次内存分配,也确保了添加单个元素操作的平均时间是一个常数时间copy(z, x)}z[len(x)] = yreturn z
}// append 方法可以追加多个元素,甚至是一个slice
var x []int
x = append(x, 1)
x = append(x, 2, 3)
x = append(x, 4, 5, 6)
// x... 表示可变的参数的slice
x = append(x, x...) 

Map

  • map中所有的key都有相同的类型,所有的value也有着相同的类型,
  • 禁止对map元素取址
  • map只能和 nil 比较

func main() {map1 := map[string]string{"name": "david","age": "22",}// 删除属性delete(map1, "age")map2 := make(map[string]string)map2["name"] = "tina"println(map1["name"])// 不存在就返回value的0值println(map2["age"])// map的遍历for key,value := range map2 {fmt.Printf("key=%s,value=%s", key, value)}map3 := map[string]string{"1倍社保": "12183","2倍社保": "24366","3倍社保": "36549",}fmt.Println(map3["1倍社保"])var ages = map[string]int {"lily": 12,}ages["bs"] = 11fmt.Println(ages == nil)fmt.Println(len(ages))fmt.Println(len(map2))// 判断属性是否存在age, ok := ages["lily"]if ok {fmt.Println("存在")fmt.Println(age)} else {fmt.Println("不存在")}
}

struct

  • 如果结构体成员名字是以大写字母开头的,那么该成员就是导出的
    type Person struct {city, Area stringage, salary intposition string
    }func structDemo1()  {var david = Person{city: "shanghai",Area: "xuhui",age: 12,position: "小队长",salary: 33333,}c := &david.city*c = "chengdu"fmt.Println(david)fmt.Println(david.city)}
    
  • 指向结构体的指针 也可以使用点操作符
    var personPointer *Person = &david
    fmt.Println(personPointer.age)
    
  • 结构体不能包含自身结构体,但是指针可以
    type Person struct {city, Area stringage, salary intposition stringPerson *Person
    }
    
  • 结构体字面量
    type Point struct{X, Y int
    }p := Point{1, 2}
    fmt.Println(p.X)
    fmt.Printf("%#v\n", p) // main.Circle{X:1, Y:2}p1 := Point{X: 2}
    p2 := Point{X: 3, Y: 3}
    fmt.Println(p1.X)
    fmt.Println(p2.X)func Scale(p Point, factor int) Point {return Point{p.X * factor, p.Y * factor}
    }fmt.Println(Scale(Point{1, 2}, 5)) // "{5 10}"
    
  • 因为结构体通常通过指针处理,可以用下面的写法来创建并初始化一个结构体变量,并返回结构体的地址:
    pointer1 := &Point{1, 2}
    
  • 如果结构体的全部成员都是可以比较的,那么结构体也是可以比较的
    p2 := Point{X: 3, Y: 3}
    p3 := Point{X: 3, Y: 3}
    println(p2 == p3) // true
    
  • 结构体嵌入和匿名成员,只声明成员数据类型而不定义成员名就叫匿名成员,匿名成员的数据类型必须是命名的类型或指向一个命名的类型的指针
    type Circle struct {X, Y, Radius int
    }type Wheel struct {X, Y, Radius, Spokes int
    }c1 := Circle{22,1,1}
    fmt.Println(c1.X)var c Circle
    c.X = 1
    c.Y = 1
    c.Radius = 1
    fmt.Println(c.X)var w Wheel
    w.X = 8
    w.Y = 8
    w.Radius = 5
    w.Spokes = 20// ---------------- 上边的大量重复代码,可以使用结构体的嵌入type Point struct {X, Y int
    }type Circle1 struct {Center PointRadius int
    }type Circle2 struct {// 只声明成员数据类型而不定义成员名就叫匿名成员// 匿名成员的数据类型必须是命名的类型或指向一个命名的类型的指针PointRadius int
    }
    
  • 得益于匿名嵌入的特性,我们可以直接访问叶子属性而不需要给出完整的路径。结构体字面值不可以简短表示匿名成员的语法。

指针

  • 指针,基本和c语言的指针是一样的
  • 任何类型的指针的零值都是nil
  • 返回函数中局部变量的地址也是安全的
    var p = f()func f() *int {v := 1return &v
    }// 每次调用f函数都将返回不同的结果:fmt.Println(f() == f()) // "false"
  • 下边代码是传入变量指针,来修改变量的值并返回
    func incr(p *int) int {*p++ // 非常重要:只是增加p指向的变量的值,并不改变p指针!!!return *p
    }v := 1
    incr(&v)             
    fmt.Println(incr(&v)) 
    
  • 另一个创建变量的方法是调用内建的new函数。表达式new(T)将创建一个T类型的匿名变量,初始化为T类型的零值,然后返回变量地址,返回的指针类型为*T
    new函数使用通常相对比较少
    p := new(int)   // p, *int 类型, 指向匿名的 int 变量
    fmt.Println(*p) // "0"
    *p = 2          // 设置 int 匿名变量的值为 2
    fmt.Println(*p) // "2"// 这两个是等价的
    func newInt() *int {return new(int)
    }func newInt() *int {var dummy intreturn &dummy
    }

json

  • 只有导出的结构体成员才会被编码,这也就是我们为什么选择用大写字母开头的成员名称。
  • 定义结构体字段的tag可以改变生产json的字段
    persons:= []Teacher{{Name: "david",Age: 22,Money: 7000,},{Name: "tom",Age: 11,Money: 3333,},
    }data, err := json.MarshalIndent(persons, "", "    ")
    if err != nil {log.Fatalf("JSON marshaling failed: %s", err)
    } else {fmt.Printf("%s\n", data)
    }var titles []Teacher
    if err := json.Unmarshal(data, &titles); err != nil {log.Fatalf("JSON unmarshaling failed: %s", err)
    }
    fmt.Println(titles)

文本和html模板


import ("log""os""text/template"
)type Student struct {Name    stringAge intHabbits []stringNormal  string
}var students = []Student{{Name:"tom", Age:19, Habbits:[]string{"dance", "write"}, Normal:"正常"},{Name:"david", Age:20, Habbits:[]string{"eat"}, Normal:"正常"},
}func addString(normal string) string {return normal + "模式"
}const templ = `students:{{range .}}-------------------------------------------------------------Name:   {{.Name}}Age:    {{.Age}}Habbits:{{range $_,$item := .Habbits}}{{$item | printf "%s\t"}}{{end}}Normal: {{.Normal | addString}}{{printf "\n"}}{{end}}`func main() {report, err := template.New("report").Funcs(template.FuncMap{"addString": addString}).Parse(templ)if err != nil {log.Fatal(err)}if err := report.Execute(os.Stdout,students); err != nil {log.Fatal(err)}}
import ("encoding/json""fmt""html/template""log""os"
)
const templ = `<!DOCTYPE html><html><meta charset="utf-8"/><head><title>主页</title></head><body><form><table align="center">{{range .}}<tr><td>Name:</td><td>{{.Name}}</td></tr><tr><td>Age:</td><td>{{.Age}}</td></tr><tr><td>Habbits:</td>{{range $_, $habbit := .Habbits}}<td>{{$habbit}}</td>{{end}}</tr><tr><td>{{.Normal | addString}}</td></tr>{{end}}</table></form><body></html>`type Student struct {Name    stringAge intHabbits []stringNormal  string
}func addString(normal string) string {return  "在" + normal
}var students = []Student{{Name:"Tom", Age:19, Habbits:[]string{"eat", "drink"},Normal:"吃饭"},{Name:"Jack", Age:20,  Habbits:[]string{"sleep"},Normal:"睡觉"},
}func main() {report, err := template.New("report").Funcs(template.FuncMap{"addString": addString}).Parse(templ)if err != nil {log.Fatal(err)}if err := report.Execute(os.Stdout, students); err != nil {log.Fatal(err)}
}

函数

  • 函数返回值可省略,也可以定义局部变量
  • 如果两个函数形式参数列表和返回值列表中的变量类型一一对应,那么这两个函数被认为有相同的类型或签名
  • 每一次调用都必须按照声明顺序为所有参数提供实参(参数值)
  • Go语言没有默认参数值
  • 实参通过值的方式传递,因此函数的形参是实参的拷贝
  • 在函数体中,函数的形参作为局部变量,被初始化为调用者提供的值。函数的形参和有名返回值作为函数最外层的局部变量,被存储在相同的词法块中
    func f1(a int, b int, c, d int) int{return a + b
    }func sub(x, y int) (z int)   {z = x - yreturn
    }
  • 多返回值
    func multiReturn(a int)(b int, c int)  {return a + 1, a * 2
    }func main() {a, b := multiReturn(22)fmt.Println(a)fmt.Println(b)
    }
    
  • 匿名函数,函数字面量,闭包
    var f1 = func(r rune) rune { return r + 1 }func squares() func() int {var x intreturn func() int {x++return x * x}
    }f := squares()
    fmt.Println(f()) // "1"
    fmt.Println(f()) // "4"
    fmt.Println(f()) // "9"
    fmt.Println(f()) // "16"
    
  • 可变参数
    func sum1(args ... int) int{return 1
    }var a = []int{1,2}
    fmt.Println(sum1(a...))
    

defer 函数

  • defer是go中一种延迟调用机制,defer后面的函数只有在当前函数执行完毕后才能执行,将延迟的语句按defer的逆序进行执行,也就是说先被defer的语句最后被执行,最后被defer的语句,最先被执行,通常用于释放资源。

  • defer语句经常被用于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁。通过defer机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放。释放资源的defer应该直接跟在请求资源的语句后。

  • defer不影响return,return之后仍然可以defer

  • 当函数遇到panic,defer仍然会被执行。Go会先执行所有的defer链表(该函数的所有defer),当所有defer被执行完毕且没有recover时,才会进行panic。所以defer 最大的功能是 panic 后依然有效,所以defer可以保证你的一些资源一定会被关闭,从而避免一些异常出现的问题。

    defer fmt.Println("defer 1")
    defer fmt.Println("defer 2")
    defer fmt.Println("defer 3")
    fmt.Println("1")
    fmt.Println("2")// 结果是
    // 1
    // 2
    // defer 3
    // defer 2
    // defer 1
    
    
    func function(index int, value int) int {fmt.Println(index)return index
    }defer function(1, function(3, 0))
    defer function(2, function(4, 0))// 结果是 3 4 2 1
    

方法

  • 在函数声明时,在其名字之前放上一个变量(类型),即是一个方法。
  • 可以看做是面向对象的一种另类实现,往某个对象添加方法,并且能够给任意类型定义方法。
    type Point struct{ X, Y float64 }// 普通函数
    func Distance(p, q Point) float64 {return math.Hypot(q.X-p.X, q.Y-p.Y)
    }// 这是一个方法,附加在p上
    func (p Point) Distance(q Point) float64 {return math.Hypot(q.X-p.X, q.Y-p.Y)
    }x := Point{1, 2}
    y := Point{4, 6}
    fmt.Println(Distance(x, y)) // "5", function call
    fmt.Println(x.Distance(y))  // "5", method call
    fmt.Println(y.Distance(x))  // "5", method call
    
  • 基于指针对象的方法,当调用函数时会对每个参数值进行拷贝,如果函数需要更新一个变量或者函数其中一个参数太大希望能够避免默认拷贝,这种情况需要用指针。对应到这里用来更新接收器的对象的方法,当这个接受者变量比较大可以用其指针而不是对象来声明方法。如果接收器本身类型就是指针,是不行的。
    func (p *Point) ScaleBy(factor float64) {p.X *= factorp.Y *= factor
    }
    r := &Point{1, 2}
    r.ScaleBy(2)
    fmt.Println(*r) // "{2, 4}"
    
  • Nil也是一个合法的接收器类型,如下是一个链表
    type IntList struct {Value intTail  *IntList
    }
    // Sum returns the sum of the list elements.
    func (list *IntList) Sum() int {if list == nil {return 0}return list.Value + list.Tail.Sum()
    }
    
  • 使用嵌入结构体来扩展类型,如果一个struct类型也可能会有多个匿名对象,那这个类型就有匿名对象所有的方法
type Point struct{ X, Y int }type ColoredPoint struct {PointColor color.RGBA
}func (p *Point) scale(size int){p.Y += sizep.X = 12
}func (p Point) Distance(q Point) int {return q.X-p.X
}func main()  {var cp ColoredPointcp.Point.Y = 2fmt.Println(cp.Y)red := color.RGBA{255, 0, 0, 255}blue := color.RGBA{0, 0, 255, 255}var p = ColoredPoint{Point{1, 1}, red}var q = ColoredPoint{Point{5, 4}, blue}fmt.Println(p.Distance(q.Point)) // 注意是q.point,不可以直接使用qp.scale(2)q.scale(2)fmt.Println(p.Distance(q.Point)) // "10"
}

错误处理

  • Go 采用明确的 error 值和类似异常的 panic 机制的方式作为独有的错误处理机制
  • 在Go中,函数运行失败时会返回错误信息,这些错误信息被认为是一种预期的值而非异常。
  • Go使用控制流机制(如if和return)处理错误。
    content, err := os.ReadFile("fa.txt")
    if err != nil {fmt.Println(err)
    }
    fmt.Println(string(content))
    

panic 异常

Go的类型系统会在编译时捕获很多错误,但有些错误只能在运行时检查,如数组访问越界、空指针引用等。这些运行时错误会引起panic异常。

一般而言,当panic异常发生时,程序会中断运行,并立即执行在该goroutine(可以先理解成线程,在第8章会详细介绍)中被延迟的函数(defer 机制)。随后,程序崩溃并输出日志信息。日志信息包括panic value和函数调用的堆栈跟踪信息。

recover

通常来说,不应该对panic异常做任何处理,但有时,也许我们可以从异常中恢复,至少我们可以在程序崩溃前,做一些操作。

  • recover必须搭配defer来使用,否则panic捕获不到;
  • defer一定要在可能引发panic的语句之前定义;
    func main() {AAA()BBB()CCC()
    }func AAA() {fmt.Println("func AAA")
    }func BBB() {defer func() {if err := recover(); err != nil {fmt.Println("recover in func BBB")fmt.Println(fmt.Sprintf("%v", err))//...handle  打日志等}}()panic("func BBB") //panic之前,如果没有recover,则程序会崩溃,异常退出。有则继续执行
    }func CCC() {fmt.Println("func CCC")
    }// func AAA
    // recover in func BBB
    // err = func BBB
    // func CCC
    

接口类型

  • 接口类型具体描述了一系列方法的集合,实现了这些方法的具体类型是这个接口类型的实例。接口是方法的集合也是一种类型。在Go 语言中是隐式实现的,意思就是对于一个具体的类型,不需要声明它实现了哪些接口,只需要提供接口所必需的方法。
    //声明一个接口
    type Human interface{Say()
    }
    //定义两个类,这两个类分别实现了 Human 接口的 Say 方法
    type women struct {}
    type man struct {}
    func (w *women) Say() {fmt.Println("I'm a women")
    }
    func(m *man) Say() {fmt.Println("I'm a man")
    }var w Human
    w = new(women)
    w.Say()
    m := new(man)
    m.Say()
    
    代码实例二
    type SalaryCalculator interface {calculate() int
    }type Staff struct {id intbase int
    }type Manager struct {id intbase intbonus int
    }func (s Staff)calculate() int {return s.base
    }func (m Manager)calculate() int {return m.base + m.bonus
    }func main() {staff1 := Staff{1, 3000}manager1 := Manager{10, 5000, 2000}employees := []SalaryCalculator{staff1, manager1}sum := 0for _, v := range employees {sum += v.calculate()}fmt.Printf("总共发工资=%d", sum)
    }
  • 接口还可以组合已有的接口,这种方式称为接口内嵌
    type Writer interface  {
    }type ReadWriter interface {ReaderWriter
    }
    
  • 通常一个具体的类型描述成一个特定的接口类型,类似OOP的多态
    var w io.Writer
    w = os.Stdout           // OK: *os.File has Write method
    w = new(bytes.Buffer)   // OK: *bytes.Buffer has Write method
    
  • interface{}被称为空接口类型是不可或缺的。因为空接口类型对实现它的类型没有要求,所以我们可以将任意一个值赋给空接口类型
    var any interface{}
    any = true
    any = 12.34
    any = "hello"
    any = map[string]int{"one": 1}
    
  • 类型断言,类型断言用于提取接口的基础值,语法:i.(T)
    func assert(i interface{}) {v, ok := i.(int)fmt.Println(v, ok)
    }
    var s interface{} = 56
    assert(s)
    var i interface{} = "Steven Paul"
    assert(i)
    
  • 类型判断,类似于类型断言,代码如下,可以判断是具体的类型。
    func findType(i interface{}) {switch i.(type) {case string:fmt.Printf("String: %s\n", i.(string))case int:fmt.Printf("Int: %d\n", i.(int))case float64:fmt.Printf("float64: %f\n", i.(float64))case Manager:fmt.Println("this is manager")default:fmt.Printf("Unknown type\n")}
    }findType(11)
    findType(11.1)
    findType("halo")
    findType(manager1)
    

反射

反射的缺点有

  1. 代码不易阅读,不易维护,容易发生线上panic
  2. 性能很差,比正常代码慢一到两个数量级

go语言反射里最重要的两个概念是TypeValue

package mainimport ("fmt""reflect"
)type Person struct {city, Area  stringage, salary intposition    stringeat         func()
}func eat() {fmt.Println("eating")
}func (p Person) fn() int {return 1
}func main() {var david = Person{city:     "shanghai",Area:     "xuhui",age:      12,position: "小队长",salary:   33333,}typeI := reflect.TypeOf(1)typeS := reflect.TypeOf("hello")type2 := reflect.TypeOf(david)fmt.Println(typeI) //intfmt.Println(typeS) //stringfmt.Println(type2) //main.Personfmt.Println(reflect.ValueOf(1))        // 1fmt.Println(reflect.ValueOf(1).Type()) // int// 获取struct成员变量的信息fieldNum := type2.NumField() //成员变量的个数for i := 0; i < fieldNum; i++ {field := type2.Field(i)fmt.Printf("%d %s offset=%d anonymous=%t type=%s exported=%t json tag %s\n", i,field.Name,            //变量名称field.Offset,          //相对于结构体首地址的内存偏移量,string类型会占据16个字节field.Anonymous,       //是否为匿名成员field.Type,            //数据类型,reflect.Type类型field.IsExported(),    //包外是否可见(即是否以大写字母开头)field.Tag.Get("json")) //获取成员变量后面``里面定义的tag}fmt.Println("-------")//获取struct成员方法的信息methodNum := type2.NumMethod() //成员方法的个数。接收者为指针的方法【不】包含在内for i := 0; i < methodNum; i++ {method := type2.Method(i)fmt.Printf("method name:%s ,type:%s, exported:%t\n", method.Name, method.Type, method.IsExported())}fmt.Println()typeFunc := reflect.TypeOf(Add) //获取函数类型fmt.Printf("is function type %t\n", typeFunc.Kind() == reflect.Func)argInNum := typeFunc.NumIn()   //输入参数的个数argOutNum := typeFunc.NumOut() //输出参数的个数for i := 0; i < argInNum; i++ {argTyp := typeFunc.In(i)fmt.Printf("第%d个输入参数的类型%s\n", i, argTyp)}for i := 0; i < argOutNum; i++ {argTyp := typeFunc.Out(i)fmt.Printf("第%d个输出参数的类型%s\n", i, argTyp)}}// 获取函数的信息func Add(a, b int) int {return a + b
}

时间

时间类型有一个自带的方法Format进行格式化,需要注意的是Go语言中格式化时间模板不是常见的Y-m-d H:M:S而是使用Go的诞生时间2006年1月2号15点04分(记忆口诀为2006 1 2 3 4)

// 定时器
timer := time.Tick(time.Second)
for v1 := range timer {fmt.Println(v1.Format("2006-01-02 15:04:05"))
}
// 貌似也可以你这么写
for{time.Sleep(time.Second)fmt.Println(v1.Format("2006-01-02 15:04:05"))
}now := time.Now()
fmt.Printf("当前时间是:%s\n", now.String())// 24小时制
fmt.Println(now.Format("2006-01-02 15:04:05.000 Mon Jan"))
// 12小时制
fmt.Println(now.Format("2006-01-02 03:04:05.000 PM Mon Jan"))
fmt.Println(now.Format("2006/01/02 15:04:05"))
fmt.Println(now.Format("2006/01/02"))fmt.Printf("%d年%d月%d日", now.Year(), now.Month(), now.Day())
fmt.Printf(" %d:%d:%d", now.Hour(), now.Minute(), now.Second())
timestamp1 := now.Unix()     //时间戳
timestamp2 := now.UnixNano() //纳秒时间戳
fmt.Println()
fmt.Printf("当前时间戳是:%v\n", timestamp1)
fmt.Printf("当前时间戳是:%v\n", timestamp2)
// 1个小时之后
endTime := now.Add(time.Minute * 60)// 时间前后比较
if now.Before(endTime) {fmt.Println("now 在 endTime 之前")
}
if now.After(endTime) {fmt.Println("now 在 endTime 之后")
}gap := endTime.Sub(now).Nanoseconds()
// 计算时间间隔
Duration := time.Unix(0, gap)
// 时间戳是自1970年1月1日(08:00:00GMT)到当前时间总毫秒数
fmt.Printf("时间相差: %d小时%d分钟%d秒", Duration.Hour()-8, Duration.Minute(), Duration.Second())// 时间由字符串转化
var a, err = time.Parse("2006-01-02", "2022-07-18")
if err != nil {fmt.Println("error")
} else {fmt.Println(a.Month())fmt.Println(a.Year())
}

命令行参数

flag库提供了命令行参数解析功能

import ("flag""fmt"
)var host stringflag.StringVar(&host, "host", "", "数据库地址")flag.Parse()fmt.Printf("数据库地址:%s\n", host)// go run main.go -host="127.0.0.1"
// 数据库地址:127.0.0.1

文件操作

io包保证任何由文件结束引起的读取失败都返回同一个错误——io.EOF

创建文件

func fileCreate() {var (newFile *os.Fileerr     error)newFile, err = os.Create("test.txt")if err != nil {log.Fatal(err)}log.Println(newFile)newFile.Close()
}

文件信息、重命名和移动、删除

func fileInfo()  {var (fileInfo os.FileInfoerr      error)err1 := os.Rename("test.txt", "new.txt")// 删除文件err2 := os.Remove("test.txt")fileInfo, err = os.Stat("test.txt")if err != nil {log.Fatal(err)}fmt.Println("File name:", fileInfo.Name())fmt.Println("Size in bytes:", fileInfo.Size())fmt.Println("Permissions:", fileInfo.Mode())fmt.Println("Last modified:", fileInfo.ModTime())fmt.Println("Is Directory: ", fileInfo.IsDir())fmt.Printf("System interface type: %T\n", fileInfo.Sys())fmt.Printf("System info: %+v\n\n", fileInfo.Sys())
}

裁剪文件

// Truncate裁剪文件 2个字节,
// 如果文件不存在就创建文件,不会报错
err := os.Truncate("a.txt", 2)if err != nil {fmt.Println("error")
}
fmt.Println("Truncate done")

读取文件

// 读取整个文件
content, err := os.ReadFile("a.txt")
if err != nil {fmt.Println("read file error")
}
fmt.Println(string(content))

写入文件,被写入文件原来的内容会被擦除

err := os.WriteFile("a.txt", []byte("Hi\n"), 0666)
if err != nil {}

包的下载

  • go get 下载的包一般会保存在这个目录 $GOPATH/src
  • 代码找包,会找$GOROOT/src然后找 $GOPATH/src
  • 如果找不到包,可以直接git clone项目到 $GOPATH/src

关于并发

基本名词

相关的名词有,进程(Process)、线程(Thread)、协程(Coroutine,也叫轻量级线程)。并发是一个cpu同时执行多个线程,在一个时间点内只有一个任务在执行。并行是多个cpu上执行多个线程,就相当于一个cpu执行一个线程,在一个时间点内有多个任务在同时执行。1.8 版本以上的会自动设置Go语言运行使用的cpu核数。每一个并发的执行单元叫作一个 goroutine协程),可以简单地把goroutine类比作一个线程。

主线程是一个物理线程,直接作用在cpu上,是重量级的,非常耗费cpu资源。协程从主线程开启,是轻量级的线程,是逻辑态,对资源的耗费相对较小,golang的协程机制是重要的特点,可以轻松的开启上万个协程(goroutine)。其他语言的并发机制是一般基于线程的,开启过多的线程耗费资源大,这里就凸显出golang的优势了,注意协程并不是go语言独有的。

goroutine使用

当一个程序启动时,其主函数即在一个单独的goroutine中运行,我们叫它main goroutine。新的goroutine会用go语句来创建。在语法上,go语句是一个普通的函数或方法调用前加上关键字go。go语句会使其语句中的函数在一个新创建的goroutine中运行。

主函数返回时,所有的goroutine都会被直接打断,程序退出。除了从主函数退出或者直接终止程序之外,没有其它的编程方法能够让一个goroutine来打断另一个的执行,但是之后可以看到一种方式来实现这个目的,通过goroutine之间的通信来让一个goroutine请求其它的goroutine,并让被请求的goroutine自行结束执行。

func main() {go spinner(100 * time.Millisecond)const n = 45fibN := fib(n) // slowfmt.Printf("\rFibonacci(%d) = %d\n", n, fibN)
}func spinner(delay time.Duration) {for {for _, r := range `-\|/` {fmt.Printf("\r%c", r)time.Sleep(delay)}}
}func fib(x int) int {if x < 2 {return x}return fib(x-1) + fib(x-2)
}

sync.Mutex 互斥锁

使用goroutine来完成,效率高,但是会出现并发/并行安全问题,不同goroutine之间如何通讯,一是全局变量互斥锁,二是使用channel来解决。如下代码,会报错 fatal error: concurrent map writes

var(myMap=make(map[int]int,10)
)//计算n!并放入到map里
func operation(n int)  {res:=1for i:=1;i<=n;i++{res*=i}myMap[n]=res
}func Test3()  {//我们开启多个协程去完成这个任务for i:=1;i<=200;i++{go operation(i)}time.Sleep(time.Second*10)fmt.Println(myMap)
}

使用互斥锁来解决问题


import ("fmt""sync""time"
)var(myMap2=make(map[int]uint64,10)//声明一个全局互斥锁,lock是一个全局互斥锁,sync是包:synchorized同步//Mutex:是互斥lock sync.Mutex
)
func operation2(n int)  {var res uint64=1for i:=1;i<=n;i++{res*=uint64(i)}//我们将res放入myMap//加锁lock.Lock()myMap2[n]= res//解锁lock.Unlock()
}func Test4()  {for i:=1;i<=200;i++{go operation2(i)}time.Sleep(time.Second*10)//这里我们输出//加锁lock.Lock()fmt.Println(myMap2)lock.Unlock()
}

使用channel

channel是一个通信机制,它可以让一个goroutine通过它给另一个goroutine发送值信息。channel本质是一个是先进先出的队列,线程安全,多个goroutine访问时,不需要加锁。channel有类型的,一个string的channel只能存放string类型的数据。

channel是引用类型,channel必须初始化才能写入数据,即make后才能用,管道是有类型的,intChan只能写入整数int。

一个channel有发送和接受两个主要操作,都是通信行为。一个发送语句将一个值从一个goroutine通过channel发送到另一个执行接收操作的goroutine。发送和接收两个操作都使用 <- 运算符。

func Test5()  {//1,创建一个可以存放3个int类型的管道var intChan chan intintChan=make(chan int,3)//2,看看intChan是什么fmt.Printf("值:%V 地址:%p\n",intChan,&intChan)//3,向管道写入数据,注意不要超过它的容量intChan<-10num:=211intChan<-numintChan<-50fmt.Printf("长度 len=%v cap=%v\n",len(intChan),cap(intChan))//4,从管道里读取数据var num2 intnum2=<-intChanfmt.Println("num2=",num2)fmt.Printf("长度 len=%v cap=%v\n",len(intChan),cap(intChan))//5,在没有使用协程的情况下,如果我们的管道数据已经全部取出,再取就会报告deadlocknum3:=<-intChannum4:=<-intChannum5:=<-intChanfmt.Println("num3=",num3,"num4=",num4,"num5=",num5)}

Channel还支持close操作,用于关闭channel,close(ch),随后对基于该channel的任何发送操作都将导致panic异常。在遍历时,如果channel没有关闭,则会出现fatal error: all goroutines are asleep - deadlock!错误。对一个已经被close过的channel进行接收操作依然可以接受到之前已经成功发送的数据;如果channel中已经没有数据的话将产生一个零值的数据。

func Test6()  {intChan := make(chan int, 5)for a := 0; a < 5; a++ {intChan<-a}close(intChan)for a := range intChan {fmt.Println(a)}
}

有缓冲channel和无缓冲channel

无缓冲channel在消息发送时需要接收者就绪。声明无缓冲channel的方式是不指定缓冲大小。示例如下

func channel1()  {c := make(chan string)var wg sync.WaitGroupwg.Add(2)go func() {defer wg.Done()// 会在发送消息foo时阻塞,原因是接收者还没有就绪c <- `foo`}()go func() {defer wg.Done()time.Sleep(time.Second * 1)println(`Message: `+ <-c)}()wg.Wait()
}

channel的结构体hchan被定义在runtime包中的chan.go文件中。以下是无缓冲channel的内部结构(本小节先介绍无缓冲channel,所以暂时忽略了hchan结构体中和缓冲相关的属性):
在这里插入图片描述
channel中持有两个链表,接收者链表recvq和发送者链表sendq,它们的类型是waitq。链表中的元素为sudog结构体类型,它包含了发送者或接收者的协程相关的信息。通过这些信息,Go可以在发送者不存在时阻塞住接收者,反之亦然。

以下是我们前一个例子的流程:

  1. 创建一个发送者列表和接收者列表都为空的channel。
  2. 第一个协程向channel发送foo变量的值,第16行。
  3. channel从池中获取一个sudog结构体变量,用于表示发送者。sudog结构体会保持对发送者所在协程的引用,以及foo的引用。
  4. 发送者加入sendq队列。
  5. 发送者协程进入等待状态。
  6. 第二个协程将从channel中读取一个消息,第23行。
  7. channel将sendq列表中等待状态的发送者出队列。
  8. chanel使用memmove函数将发送者要发送的值进行拷贝,包装入sudog结构体,再传递给channel接收者的接收变量。
  9. 在第五步中被挂起的第一个协程将恢复运行并释放第三步中获取的sudog结构体。

如流程所描述,发送者协程阻塞直至接收者就绪。但是,必要的时候,我们可以使用有缓冲channel来避免这种阻塞。

channel可以声明为只读或者只写

// 只写
var chan1 chan <- int
// 只读
var chan2 <- chan int

channel使用实例

开启一个writeData协程,向管道intChan中写入五十个数
开启一个readDate协程,从管道intChan中读取writeData写入数据
WriteData和readData操作的是同一个管道
主线程需要等待writeData和readData协程都完成工作才能退出管道


var intChan = make(chan int, 10)
var exitChan =make(chan bool,1)func writeData()  {for a:=0; a< 10; a++ {intChan<-afmt.Printf("write a = %d\n", a)}close(intChan)
}func readData()  {for a:= range intChan {fmt.Printf("read a = %d\n", a)}exitChan<-trueclose(exitChan)
}func main()  {go writeData()go readData()for {_,ok:=<-exitChanfmt.Println("finish")if!ok{break}}
}

sync.Waitgroup

go 里面的 WaitGroup 是非常常见的一种并发控制方式,它可以让我们的代码等待一组 goroutine 的结束。

import ("fmt""sync"
)func waitGroupFn() {var wg sync.WaitGroup// 计数器 +2wg.Add(2)go func() {sendHttpRequest("https://baidu.com")// 计数器 -1wg.Done()}()go func() {sendHttpRequest("https://baidu.com")// 计数器 -1wg.Done()}()// 在函数的最后,我们调用了 wg.Wait,这个方法会阻塞,// 直到 WaitGroup 的计数器的值为 0 才会解除阻塞状态wg.Wait()
}// 发起 HTTP GET 请求
func sendHttpRequest(url string)  {fmt.Println(url)
}func main()  {waitGroupFn()
}

WaitGroup 内部通过一个计数器来统计有多少协程被等待。这个计数器的值在我们启动 goroutine 之前先使用 Add 方法写入,然后在 goroutine 结束的时候,使用 Done 方法将这个计数器减 1。在启动这些 goroutine 的协程中会调用 Wait 来进行等待,在 Wait 调用的地方会阻塞,直到 WaitGroup 内部的计数器减到 0,也就实现了等待一组 goroutine 的目的。

信号量

在操作系统中,有多种实现进程/线程间同步的方式,如:test_and_setcompare_and_swap互斥锁等。 除此之外,还有一种是信号量,它的功能类似于互斥锁,但是它能提供更为高级的方法,以便进程能够同步活动。

扩展 - bit数组

数据流分析领域,集合元素通常是一个非负整数,集合会包含很多元素,并且集合会经常进行并集、交集操作,这种情况下,bit数组会比map表现更加理想。 再比如我们执行一个http下载任务,把文件按照16kb一块划分为很多块,需要有一个全局变量来标识哪些块下载完成了,这种时候也需要用到bit数组。


// An IntSet is a set of small non-negative integers.
// Its zero value represents the empty set.
type IntSet struct {words []uint64
}// Has reports whether the set contains the non-negative value x.
func (s *IntSet) Has(x int) bool {word, bit := x/64, uint(x%64)return word < len(s.words) && s.words[word]&(1<<bit) != 0
}// Add adds the non-negative value x to the set.
func (s *IntSet) Add(x int) {word, bit := x/64, uint(x%64)for word >= len(s.words) {s.words = append(s.words, 0)}s.words[word] |= 1 << bit
}// UnionWith sets s to the union of s and t.
func (s *IntSet) UnionWith(t *IntSet) {for i, tword := range t.words {if i < len(s.words) {s.words[i] |= tword} else {s.words = append(s.words, tword)}}
}// String returns the set as a string of the form "{1 2 3}".
func (s *IntSet) String() string {var buf bytes.Bufferbuf.WriteByte('{')for i, word := range s.words {if word == 0 {continue}for j := 0; j < 64; j++ {if word&(1<<uint(j)) != 0 {if buf.Len() > len("{") {buf.WriteByte(' ')}fmt.Fprintf(&buf, "%d", 64*i+j)}}}buf.WriteByte('}')return buf.String()
}

参考链接和扩展资料

文档类

  • 官网
  • go语言圣经(这本书的话啰嗦而且讲不清东西)
  • go学习路线
  • go入门指南
  • go语言101
  • go高级编程
  • go语言高级编程
  • effective go
  • go标准库
  • 地鼠文档

知识点类

  • 详谈GoLang的panic和error
  • vs code配置go开发环境
  • Go 变量(var) & 常量(const)
  • 一文搞懂Go语言中defer的使用
  • 如何优雅的使用Go接口?
  • Go语言的30个常用文件操作,总有一个你会用到
  • Go Modules
  • 《GO语言圣经》:Bit数组实例讲解
  • goGin框架教程
  • 我为什么放弃Go语言?
  • Golang interface 接口全面理解 (一)
  • Golang interface 接口全面理解 (二)
  • GO语言之Goroutine和channel
  • Go语言的有缓冲channel和无缓冲channel
  • 深入理解 go sync.Waitgroup
  • 一篇带你全面掌握go反射的用法

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/125015.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【设计模式】一、设计模式七大原则

文章目录 设计模式概述设计模式七大原则设计模式的目的设计模式七大原则1. 单一职责原则2. 接口隔离原则3. 依赖倒转(倒置)原则4. 里氏替换原则5. 开闭原则&#xff08;Open-Closed Principle简称OCP原则&#xff09;6. 迪米特法则7. 合成复用原则&#xff08;Composite Reuse …

ABB REF615C-D HCFFAEAGABC2BAA1XD控制继电器

多功能保护&#xff1a;REF615C-D 继电器具备多种保护功能&#xff0c;包括过流、短路、地故障、欠频、过频、欠电压、过电压等&#xff0c;可用于监测和保护电力系统中的设备。 通信能力&#xff1a;该继电器支持通信协议&#xff0c;如IEC 61850、Modbus等&#xff0c;使其能…

YOLOv5算法改进(11)— 替换主干网络之EfficientNetv2

前言&#xff1a;Hello大家好&#xff0c;我是小哥谈。EfficientNetV2是一个网络模型&#xff0c;旨在提供更小的模型和更快的训练速度。它是EfficientNetV1的改进版本。EfficientNetV2通过使用更小的模型参数和采用一种称为Progressive Learning的渐进学习策略来实现这一目标。…

Benchmarking Chinese Text Recognition: Datasets, Baselines| OCR 中文数据集【论文翻译】

基础信息如下 https://arxiv.org/pdf/2112.15093.pdfhttps://github.com/FudanVI/benchmarking-chinese-text-recognition Abstract 深度学习蓬勃发展的局面见证了近年来文本识别领域的迅速发展。然而&#xff0c;现有的文本识别方法主要针对英文文本。作为另一种广泛使用的语…

NIFI实现JSON转SQL并插入到数据库表中

说明 本文中的NIFI是使用docker进行安装的&#xff0c;所有的配置参考&#xff1a;docker安装Apache NIFI 需求背景 现在有一个文件&#xff0c;里面存储的是一些json格式的数据&#xff0c;要求将文件中的数据存入数据库表中&#xff0c;以下是一些模拟的数据和对应的数据库…

初试小程序轮播组件

文章目录 一、轮播组件&#xff08;一&#xff09;swiper组件1、功能描述2、属性说明 &#xff08;二&#xff09;swiper-item组件1、功能描述2、属性说明 二、案例演示&#xff08;一&#xff09;运行效果&#xff08;二&#xff09;实现步骤1、创建小程序项目2、准备图片素材…

工程管理系统简介 工程管理系统源码 java工程管理系统 工程管理系统功能设计

鸿鹄工程项目管理系统 Spring CloudSpring BootMybatisVueElementUI前后端分离构建工程项目管理系统 1. 项目背景 一、随着公司的快速发展&#xff0c;企业人员和经营规模不断壮大。为了提高工程管理效率、减轻劳动强度、提高信息处理速度和准确性&#xff0c;公司对内部工程管…

微信小程序 房贷计算器 js代码终极版

这里写目录标题 展示图1.在utils 中创建文件calculateMortgage.ts2. 在需要使用的地方引入并传参 展示图 1.在utils 中创建文件calculateMortgage.ts /** 假设房贷本金是60万&#xff0c;贷款年利率为4.8%即月利率为0.4%&#xff0c;分20年即240期偿还&#xff0c;等额本金还款…

AWS-数据库迁移工具DMS-场景:单账号跨区域迁移RDS for Mysql

参考文档&#xff1a; 分为几个环节&#xff1a; 要使用 AWS DMS 迁移至 Amazon RDS 数据库实例&#xff1a; 1.创建复制实例 有坑内存必须8g或者以上&#xff0c;我测试空库 都提示内存不足 2.创建目标和源终端节点 目标空库也得自己创建哈 3.刷新源终端节点架构 4.创建迁…

echarts环图配置

echarts环图配置 1、安装echarts npm install echarts4.9.02、页面引入echarts import echarts from echarts;3、应用 template片段 <div class"chart-wrap"><div id "treeChart" style "width: 180px; height:180px;" ><…

vite介绍

vite vite是一种新的前端构建工具&#xff0c;vite借助了浏览器对ESM的支持&#xff0c;采用和传统webpack打包完全不一致的unbundle打包机制&#xff1b; vite的快主要体现在两个方面&#xff0c;快速的冷启动和快速的热更新 快速的冷启动&#xff1a;vite只需启动一台静态页…

LeetCode刷题笔记【24】:贪心算法专题-2(买卖股票的最佳时机II、跳跃游戏、跳跃游戏II)

文章目录 前置知识122.买卖股票的最佳时机II题目描述贪心-直观写法贪心-优化代码更简洁 55. 跳跃游戏题目描述贪心-借助ability数组贪心-只用int far记录最远距离 45.跳跃游戏II题目描述回溯算法贪心算法 总结 前置知识 参考前文 参考文章&#xff1a; LeetCode刷题笔记【23】…

SpringBoot隐藏文件

1.设置 2.输入file Types 3.点击忽略文件或者文件夹 4.成功

Linux下go环境安装、环境配置并执行第一个go程序

一、安装 1.Golang对Linux的内核版本要求 GO对Linux内核版本最低要求是 2.6.23&#xff0c;对应要求操作系统版本是&#xff1a; RHEL 6.0CentOS 6.0即&#xff0c;不支持 (RHEL 和 CentOS) 的 (4.x or 5.x)。2.下载golang的代码版本 Golang的官网下载地址&#xff1a;https:…

Qt 简单闹钟

//wiget.h#ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QTime> //时间类 #include <QTimer> //定时器类 #include <QTextToSpeech> #include <QDebug> QT_BEGIN_NAMESPACE namespace Ui { class Widget; } QT_END_NAMESPA…

文件上传与下载

文章目录 1. 前端要求2. 后端要求 1. 前端要求 //采用post方法提交文件 method"post" //采用enctype属性 enctype"" //type属性要求 type"file"2. 后端要求 package com.itheima.reggie.controller;import com.itheima.reggie.common.R; impo…

(数字图像处理MATLAB+Python)第十二章图像编码-第三、四节:有损编码和JPEG

文章目录 一&#xff1a;有损编码&#xff08;1&#xff09;预测编码A&#xff1a;概述B&#xff1a;DM编码C&#xff1a;最优预测器 &#xff08;2&#xff09;变换编码A&#xff1a;概述B&#xff1a;实现变换编码的主要问题 二&#xff1a;JPEG 一&#xff1a;有损编码 &am…

README

一、Markdown 简介 Markdown 是一种轻量级标记语言&#xff0c;它允许人们使用易读易写的纯文本格式编写文档。 应用 当前许多网站都广泛使用 Markdown 来撰写帮助文档或是用于论坛上发表消息。例如&#xff1a;GitHub、简书、知乎等 编辑器 推荐使用Typora&#xff0c;官…

使用Akka的Actor模拟Spark的Master和Worker工作机制

使用Akka的Actor模拟Spark的Master和Worker工作机制 Spark的Master和Worker协调工作原理 在 Apache Spark 中&#xff0c;Master 和 Worker 之间通过心跳机制进行通信和保持活动状态。下面是 Master 和 Worker 之间心跳机制的工作流程&#xff1a; Worker 启动后&#xff0c…

Redis 7 第九讲 微服务集成Redis 应用篇

Jedis 理论 Jedis是redis的java版本的客户端实现&#xff0c;使用Jedis提供的Java API对Redis进行操作&#xff0c;是Redis官方推崇的方式&#xff1b;并且&#xff0c;使用Jedis提供的对Redis的支持也最为灵活、全面&#xff1b;不足之处&#xff0c;就是编码复杂度较高。 …