Go 语言快速入门

ZX 觉得这个能一天看完,但我啥也没记住,只好再看一遍

Posted by WYX on November 28, 2021

Go 语言快速入门

这里现在很多东西只有标题,主要是让我记起来“Golang这玩意有啥特点”,内容慢慢填吧,坑了(

参考资料

0. 入门例程

并发和获取 URL

// Fetchall fetches URLs in parallel and reports their times and sizes.
package main

import (
    "fmt"
    "io"
    "io/ioutil"
    "net/http"
    "os"
    "time"
)

func main() {
    start := time.Now()
    ch := make(chan string)
    for _, url := range os.Args[1:] {
        go fetch(url, ch) // start a goroutine
    }
    for range os.Args[1:] {
        fmt.Println(<-ch) // receive from channel ch
    }
    fmt.Printf("%.2fs elapsed\n", time.Since(start).Seconds())
}

func fetch(url string, ch chan<- string) {
    start := time.Now()
    resp, err := http.Get(url)
    if err != nil {
        ch <- fmt.Sprint(err) // send to channel ch
        return
    }
    nbytes, err := io.Copy(ioutil.Discard, resp.Body)
    resp.Body.Close() // don't leak resources
    if err != nil {
        ch <- fmt.Sprintf("while reading %s: %v", url, err)
        return
    }
    secs := time.Since(start).Seconds()
    ch <- fmt.Sprintf("%.2fs  %7d  %s", secs, nbytes, url)
}

Web 服务

// Server2 is a minimal "echo" and counter server.
package main

import (
    "fmt"
    "log"
    "net/http"
    "sync"
)

var mu sync.Mutex
var count int

func main() {
    http.HandleFunc("/", handler)
    http.HandleFunc("/count", counter)
    log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

// handler echoes the Path component of the requested URL.
func handler(w http.ResponseWriter, r *http.Request) {
    mu.Lock()
    count++
    mu.Unlock()
    fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
}

// counter echoes the number of calls so far.
func counter(w http.ResponseWriter, r *http.Request) {
    mu.Lock()
    fmt.Fprintf(w, "Count %d\n", count)
    mu.Unlock()
}

1. 基本写法、结构

Go 语言推荐驼峰命名;

Go 没有 ?: 三元运算符;

在这里一个变量/包声明了不用是会报错的,所以不要浪费;

var 声明变量

var i, j, k int                  // int, int, int
var b, f, s = true, 2.3, "four"  // bool, float64, string
var f, err = os.Open(name)       // 多个返回值初始化,os.Open() 也许返回一个文件,也许是 error

const 声明常量

常量的运算必须可以在编译期完成

常量可以没有显式类型,常量的形式将隐式决定变量的默认类型

const (
    e  = 2.71828182845904523536028747135266249775724709369995957496696763
    pi = 3.14159265358979323846264338327950288419716939937510582097494459
)

iota 常量生成器

生成一组以相似规则初始化的常量,而不必每行都写一遍初始化表达式

type Weekday int

const (
    Sunday Weekday = iota // = 0, 1, 2 ...
    Monday
    Tuesday
    Wednesday
    Thursday
    Friday
    Saturday
)

type Flags uint

const (
    FlagUp Flags = 1 << iota // is up
    FlagBroadcast            // supports broadcast access capability
    FlagLoopback             // is a loopback interface
    FlagPointToPoint         // belongs to a point-to-point link
    FlagMulticast            // supports multicast access capability
)

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

简短变量声明 :=

简短变量声明语句中必须 至少要声明一个新的变量

f, err := os.Open(name)
if err != nil {
    return err
}
// ...use f...
f.Close()

// 一般i为下标,c为内容
for i, c := range s {
    if m[int(c)] == 1 {
        return i
    }
}

指针

和 C/C++ 差不多;

任何类型的指针的零值都是 nil

x := 1
p := &x         // p, of type *int, points to x
fmt.Println(*p) // "1"
*p = 2          // equivalent to x = 2
fmt.Println(x)  // "2"

new(T) 函数

表达式 new(T) 创建一个T类型的匿名变量,初始化为T类型的零值,返回变量地址,指针类型为*T

p := new(int)    // p, *int 类型, 指向匿名的 int 变量
fmt.Println(*p)  // "0"
*p = 2           // 设置 int 匿名变量的值为 2
fmt.Println(*p)  // "2"

变量赋值

x = 1                       // 命名变量的赋值
*p = true                   // 通过指针间接赋值
person.name = "bob"         // 结构体字段赋值
count[x] = count[x] * scale // 数组、slice或map的元素赋值,也可以 count[x] *= scale
i, j, k = 2, 3, 5
a[i], a[j] = a[j], a[i]     // 元组赋值语句,交换变量的值

// 求最大公约数
func gcd(x, y int) int {
    for y != 0 {
        x, y = y, x%y
    }
    return x
}

// 计算斐波纳契数列的第N个数
func fib(n int) int {
    x, y := 0, 1
    for i := 0; i < n; i++ {
        x, y = y, x+y
    }
    return x
}

Go 的自增和自减是语句,而不是表达式

v++      // 等价方式 v = v + 1;v 变成 2
v--      // 等价方式 v = v - 1;v 变成 1
x = i++  // 错误!

会产生多个值的表达式

map查找、类型断言、或通道接收出现在赋值语句的右边时,并不一定产生两个结果,也可能只产生一个结果;

对于只产生一个结果的情形:

  • map查找失败时会返回零值;
  • 类型断言失败时会发生运行时panic异常;
  • 通道接收失败时会返回零值(阻塞不算是失败)
v, ok = m[key]             // map lookup
v, ok = x.(T)              // type assertion
v, ok = <-ch               // channel receive

v = m[key]                // map查找,失败时返回零值
v = x.(T)                 // type断言,失败时panic异常
v = <-ch                  // 管道接收,失败时返回零值(阻塞不算是失败)

_, ok = m[key]            // map返回2个值
_, ok = mm[""], false     // map返回1个值
_ = mm[""]                // map返回1个值

下划线空白标识符 _

可以丢弃不需要的值

_, err = io.Copy(dst, src) // 丢弃字节数
_, ok = x.(T)              // 只检测类型,忽略具体值

type 声明类型

package tempconv

import "fmt"

type Celsius float64    // 摄氏温度
type Fahrenheit float64 // 华氏温度

const (
    AbsoluteZeroC Celsius = -273.15 // 绝对零度
    FreezingC     Celsius = 0       // 结冰点温度
    BoilingC      Celsius = 100     // 沸水温度
)

func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }

func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }

作用域

声明语句的作用域:

  • 对应的是一个源代码的文本区域;
  • 是一个编译时的属性

变量的生命周期:

  • 指程序运行时变量存在的有效时间段,在此时间区域内它可以被程序的其他部分引用;
  • 是一个运行时的概念。

Go语言的习惯是在 if 中处理错误然后直接返回;

f, err := os.Open(fname)
if err != nil {
    return err
}
f.ReadByte()
f.Close()

变量的生命周期和垃圾回收

Go 语言有自动的内存回收机制;

2. 数据类型

数据类型注意事项

  • len() 返回的是有符号的 int
  • 浮点数有 float32float64
  • 浮点数到整数的转换将丢失小数部分,向数轴零方向截断
  • 布尔型有 truefalse
  • 字符串存UTF8编码的Unicode,越界会 panic
    • 字符串的值是不可变的

  • 复数有 complex64complex128
var x complex128 = complex(1, 2) // 1+2i
var y complex128 = complex(3, 4) // 3+4i
fmt.Println(x*y)                 // "(-5+10i)"
fmt.Println(real(x*y))           // "-5"
fmt.Println(imag(x*y))           // "10"

类型转换写法 T(x)

f := 3.141 // a float64
i := int(f)
fmt.Println(f, i) // "3.141 3"
f = 1.99
fmt.Println(int(f)) // "1"

var u uint8 = 255
fmt.Println(u, u+1, u*u) // "255 0 1"

var i int8 = 127
fmt.Println(i, i+1, i*i) // "127 -128 1"

位运算操作符

运算符 作用
& 位运算 AND
| 位运算 OR
^ 位运算 XOR
&^ 位清空 AND NOT
« 左移
>> 右移

fmt.Printf() 格式化输出

  • [1] 副词告诉Printf函数再次使用第一个操作数;
  • # 副词告诉Printf在用 %o、%x、%X 输出时带有 0、0x、0X 前缀;
  • %c 参数打印字符;
  • %q 参数打印带单引号的字符:
    • 其他方面和 C/C++ 差不多
o := 0666
fmt.Printf("%d %[1]o %#[1]o\n", o) 
// "438 666 0666"

x := int64(0xdeadbeef)
fmt.Printf("%d %[1]x %#[1]x %#[1]X\n", x)
// Output:
// 3735928559 deadbeef 0xdeadbeef 0XDEADBEEF

ascii := 'a'
unicode := '国'
newline := '\n'
fmt.Printf("%d %[1]c %[1]q\n", ascii)   // "97 a 'a'"
fmt.Printf("%d %[1]c %[1]q\n", unicode) // "22269 国 '国'"
fmt.Printf("%d %[1]q\n", newline)       // "10 '\n'"

字符串

s[i:j] 取子字符串

s := "hello, world"
fmt.Println(s[0:5]) // "hello"
fmt.Println(s[:5])  // "hello"
fmt.Println(s[7:])  // "world"
fmt.Println(s[:])   // "hello, world"
fmt.Println("goodbye" + s[5:])  // "goodbye, world"

`…` 原生的字符串字面值

  • 会删除回车
  • 使用反引号代替双引号
  • 没有转义操作,包含退格和换行
  • 用于编写正则表达式、HTML模板、JSON面值、命令行提示信息
  • 无法直接写 ` 字符,可以用八进制或十六进制转义或 + “`” 连接字符串常量
const GoUsage = `Go is a tool for managing Go source code.

处理字符串和字节slice

Golang 标准库文档链接

// import "strconv"

x := 123
y := fmt.Sprintf("%d", x)
fmt.Println(y, strconv.Itoa(x)) // "123 123"
fmt.Println(strconv.FormatInt(int64(x), 2)) // "1111011"
x, err := strconv.Atoi("123")             // x is an int
y, err := strconv.ParseInt("123", 10, 64) // base 10, up to 64 bits
  • strings 包提供了许多如字符串的查询、替换、比较、截断、拆分和合并等功能。

  • bytes 包也提供了很多类似功能的函数,但是针对和字符串有着相同结构的 []byte 类型; string字符串是只读的,因此逐步构建字符串会导致很多分配和复制,使用 bytes.Buffer 类型将会更有效;

  • strconv 包提供了布尔型、整型数、浮点数和对应字符串的相互转换,还提供了双引号转义相关的转换。

  • unicode 包提供了 IsDigit、IsLetter、IsUpper、IsLower 等类似功能,用于给字符分类

数组

var a [3]int 
var q [3]int = [3]int{1, 2, 3}
fmt.Println(a[0])        
fmt.Println(a[len(a)-1]) 

// 遍历数组,取下标和值
for i, v := range a {
    fmt.Printf("%d %d\n", i, v)
}

// 只取值
for _, v := range a {
    fmt.Printf("%d\n", v)
}

// 也可以根据初始化值的个数计算数组长度
q := [...]int{1, 2, 3}

// 数组r含有100个元素,最后一个元素被初始化为-1,其它元素都是0
r := [...]int{99: -1}

// 数组之间有 == 和 != 运算符
a := [2]int{1, 2}
b := [...]int{1, 2}
c := [2]int{1, 3}
fmt.Println(a == b, a == c, b == c) // "true false false"
d := [3]int{1, 2}
fmt.Println(a == d) // compile error: cannot compare [2]int == [3]int

Slice

应该是可以代替 C++ 的大多数顺序容器,只不过需要开脑洞合理利用一下切片功能

对着 LeetCode 评论区无能狂怒:我怎么就想不出来呢

// 本质变长序列,底层是指针、长度、容量
months := [...]string{1: "January", /* ... */, 12: "December"}

// len和cap函数分别返回slice的长度和容量
len(s) == 0    // 判空

// 不同于数组,== 操作符只能和 nil 比
var s []int    // len(s) == 0, s == nil
s = nil        // len(s) == 0, s == nil
s = []int(nil) // len(s) == 0, s == nil
s = []int{}    // len(s) == 0, s != nil

// slice可以用来模拟一个stack
stack = append(stack, v)     // 入栈
top := stack[len(stack)-1]   // 取栈顶元素,记得确保len(stack)-1 >= 0
stack = stack[:len(stack)-1] // 出栈

// 删除中间的某个元素
func remove(slice []int, i int) []int {
    copy(slice[i:], slice[i+1:])
    return slice[:len(slice)-1]
}

// sort 排序,使用了 sort.Interface
sort.Slice(arr, func(i, j int) bool { return arr[i].score < arr[j].score }) // 升序排序

make() 函数创建 slice

创建一个指定元素类型、长度和容量的 slice,容量部分可省略

make([]T, len)
make([]T, len, cap) // same as make([]T, cap)[:len]

// 开一个r行c列空数组
res := make([][]int, r)
for i := range res {
    res[i] = make([]int, c)
}

append() 函数向slice追加元素

var runes []rune
for _, r := range "Hello, 世界" {
    runes = append(runes, r)
}

// 第二个参数也可以是Slice,但是后面要加...
badOranges = append(badOranges, nextBadOranges...)

Map

Slice 和 Map 都只能和 nil 比,不能像数组那样互相比;

// map 是哈希表,可以写为 map[K]V 
ages := make(map[string]int) 

// 也可以花括号初始化
ages := map[string]int{
    "alice":   31,
    "charlie": 34,
}

// Go 没提供查询一个key在不在map中的办法,但是可以利用方便的变量赋值实现
// _, ok := map[key];语句查找成功返回两个值,ok=true,查找失败时返回零值,ok=false
// ok 的布尔值是 if 的最终结果 
if _, ok := map[key]; ok {
    // 存在
}

// 使用了fmt.Sprintf函数将字符串列表转换为一个字符串用作map的key
var m = make(map[string]int)
func k(list []string) string  { return fmt.Sprintf("%q", list) }
func Add(list []string)       { m[k(list)]++ }
func Count(list []string) int { return m[k(list)] }

// Go 语言没有 set,可以用忽略值只用键的 map 代替;
seen := make(map[string]struct{})  // a set of strings

delete() 函数删除 map 中元素

delete(ages, "alice") // remove element ages["alice"]

结构体

成员顺序不同的结构体是不相同的;

大写字母开头的成员就是 public 的;

// 定义
type Employee struct {
    ID        int
    Name, Address   string
    DoB       time.Time
    Position  string
    Salary    int
    ManagerID int
}

// 新建实例
var dilbert Employee

// 访问成员
dilbert.Salary -= 5000 // demoted, for writing too few lines of code
position := &dilbert.Position
*position = "Senior " + *position // promoted, for outsourcing to Elbonia
var employeeOfTheMonth *Employee = &dilbert
employeeOfTheMonth.Position += " (proactive team player)"

// 返回结构体的函数
func EmployeeByID(id int) *Employee { /* ... */ }
fmt.Println(EmployeeByID(dilbert.ManagerID).Position) // "Pointy-haired boss"
id := dilbert.ID
EmployeeByID(id).Salary = 0  // fired for... no real reason

// 结构体字面值表示方法1
type Point struct{ X, Y int }
p := Point{1, 2}

// 结构体字面值表示方法2,以成员名字和相应的值来初始化
anim := gif.GIF{LoopCount: nframes}

// 修改结构体内容的函数
func AwardAnnualRaise(e *Employee) {
    e.Salary = e.Salary * 105 / 100
}

// 直接返回地址
pp := &Point{1, 2}   /* 和下面两条语句是等价的
pp := new(Point)
*pp = Point{1, 2} */

// 如果所有成员都是可以比较的,那么该结构体也是可以用 == 和 != 比较的;
p := Point{1, 2}
q := Point{2, 1}
fmt.Println(p.X == q.X && p.Y == q.Y) // "false"

// 匿名成员,只声明一个成员对应的数据类型而不指名成员的名字
type Point struct {
    X, Y int
}

type Circle struct {
    Point
    Radius int
}

type Wheel struct {
    Circle
    Spokes int
}

var w Wheel
w.X = 8            // equivalent to w.Circle.Point.X = 8
w.Y = 8            // equivalent to w.Circle.Point.Y = 8
w.Radius = 5       // equivalent to w.Circle.Radius = 5
w.Spokes = 20

数据结构常用的结构体

// 单链表
type ListNode struct {
    Val  int
    Next *ListNode
}

// 二叉树
type TreeNode struct {
    Val int
    Left *TreeNode
    Right *TreeNode
}

// 递归实现中序遍历二叉树
func inorderTraversal(root *TreeNode) []int {
    var res []int
    var inorder func(*TreeNode)
    inorder = func(node *TreeNode) {
        if node == nil {
            return
        }
        inorder(node.Left)
        res = append(res, node.Val)
        inorder(node.Right)
    }
    inorder(root)
    return res
}

JSON 的对象类型

标准库有一些包提供支持:

  • encoding/json
  • encoding/xml
  • encoding/asn1
boolean         true
number          -273.15
string          "She said \"Hello, BF\""
array           ["gold", "silver", "bronze"]
object          {"year": 1980,
                 "event": "archery",
                 "medals": ["gold", "silver", "bronze"]}
type Movie struct {
    Title  string
    // 结构体成员Tag, 通常是一系列用空格分隔的key:"value"键值对序列, 键名:对象名,选项 
    // omitempty选项表示当Go语言结构体成员为空或零值时,不生成该JSON对象
    Year   int  `json:"released"`         
    Color  bool `json:"color,omitempty"` 
    Actors []string
}

var movies = []Movie{
    {Title: "Casablanca", Year: 1942, Color: false,
        Actors: []string{"Humphrey Bogart", "Ingrid Bergman"}},
    {Title: "Cool Hand Luke", Year: 1967, Color: true,
        Actors: []string{"Paul Newman"}},
    {Title: "Bullitt", Year: 1968, Color: true,
        Actors: []string{"Steve McQueen", "Jacqueline Bisset"}},
    // ...
}

// 结构体slice转为JSON
data, err := json.MarshalIndent(movies, "", "    ")
if err != nil {
    log.Fatalf("JSON marshaling failed: %s", err)
}
fmt.Printf("%s\n", data)

文本和HTML模板

标准库有一些包提供支持,遇事不决查文档:

  • text/template
  • html/template
$ go doc html/template
const templ = ` issues:
----------------------------------------
Number: 
User:   
Title:  
Age:     days
`

3. 函数

func 声明函数

  • 函数值之间不可比较;
  • 不能用函数值作为map的key
  • 函数像其他值一样,拥有类型;
  • 函数可以被赋值给其他变量,传递给函数,作为函数返回值;
func name(parameter-list) (result-list) {
    body
}

// 等价于func f(i int, j int, k int,  s string, t string)
func f(i, j, k int, s, t string) { /* ... */ } 

// 空白标识符依然能用,可以忽略第二个参数而不影响其函数签名
func first(x int, _ int) int { return x }

// 可变参数个数
func sum(vals ...int) int {
    total := 0
    for _, val := range vals {
        total += val
    }
    return total
}

// 函数赋值给变量
var preorder func(*TreeNode)
preorder = func(node *TreeNode) {
    if node == nil {
        return
    }
    res = append(res, node.Val)
    preorder(node.Left)
    preorder(node.Right)
}
preorder(root)    // 调用

递归

Go 语言自带可变栈,栈的大小按需增加,所以使用递归时不必考虑溢出和安全问题;

/* Leetcode 21. 合并两个有序链表 */

type ListNode struct {
    Val  int
    Next *ListNode
}

func mergeTwoLists(list1 *ListNode, list2 *ListNode) *ListNode {
    if list1 == nil {
        return list2
    } else if list2 == nil {
        return list1
    } else if list1.Val < list2.Val {
        // 递归设计思路:让较小结点的 next 指针指向其余结点的合并结果
        list1.Next = mergeTwoLists(list1.Next, list2)
        return list1
    } else {
        list2.Next = mergeTwoLists(list1, list2.Next)
        return list2
    }
}

多返回值的函数

// 函数直接 return res1, res2 就行
func findLinks(url string) ([]string, error) {
    resp, err := http.Get(url)
    if err != nil {
        return nil, err
    }
    if resp.StatusCode != http.StatusOK {
        resp.Body.Close()
        return nil, fmt.Errorf("getting %s: %s", url, resp.Status)
    }
    doc, err := html.Parse(resp.Body)
    resp.Body.Close()
    if err != nil {
        return nil, fmt.Errorf("parsing %s as HTML: %v", url, err)
    }
    return visit(nil, doc), nil
}

// 调用时应该显式分配这多个返回值
links, err := findLinks(url)
links1, _ := findLinks(url) 

// 按照惯例,函数的最后一个bool类型的返回值表示函数是否运行成功,error类型的返回值代表函数的错误信息
// 每一个return语句等价于:return words, images, err
func CountWordsAndImages(url string) (words, images int, err error) {
    resp, err := http.Get(url)
    if err != nil {
        return
    }
    doc, err := html.Parse(resp.Body)
    resp.Body.Close()
    if err != nil {
        err = fmt.Errorf("parsing HTML: %s", err)
        return
    }
    words, images = countWordsAndImages(doc)
    return
}

func countWordsAndImages(n *html.Node) (words, images int) { /* ... */ }

错误处理

方法1:返回错误信息

doc, err := html.Parse(resp.Body)
resp.Body.Close()
if err != nil {
    // 注意错误信息中应避免大写和换行符
    return nil, fmt.Errorf("parsing %s as HTML: %v", url,err) 
}

// 输出函数获得字符串类型的错误信息
fmt.Println(err)
fmt.Printf("%v", err)

方法2:重复尝试操作

for tries := 0; time.Now().Before(deadline); tries++ {
    _, err := http.Head(url)
    if err == nil {
        return nil // success
    }
    log.Printf("server not responding (%s);retrying…", err)
    time.Sleep(time.Second << uint(tries)) // exponential back-off
}

方法3:输出错误信息并结束程序

  • 应该只在main中执行这种操作,库函数一般只应该传播错误,除非遇到bug
// (In function main.)
if err := WaitForServer(url); err != nil {
    fmt.Fprintf(os.Stderr, "Site is down: %v\n", err)
    os.Exit(1)
}

// 更标准的写法
if err := WaitForServer(url); err != nil {
    log.Fatalf("Site is down: %v\n", err)
}

// 也可以只输出错误信息
if err := Ping(); err != nil {
    log.Printf("ping failed: %v; networking disabled",err)
}

匿名函数

优点在于可以访问它所在作用域里的全部变量

// squares返回一个匿名函数, 该匿名函数每次被调用时都会返回下一个数的平方。
func squares() func() int {
    var x int
    return func() int {
        x++
        return x * x
    }
}

defer 语句

当执行到含有关键字 defer 的语句时,函数和参数表达式得到计算,但直到包含该defer语句的函数执行完毕时,defer后的函数才会被执行

// 例:互斥锁
var mu sync.Mutex
var m = make(map[string]int)
func lookup(key string) int {
    mu.Lock()
    defer mu.Unlock()
    return m[key]
}

panic 异常和 recover 捕获异常

当panic异常发生时:

  • 程序会中断运行,并立即执行在该 goroutine 中的 defer 函数
  • 随后程序崩溃并输出日志信息
    • 日志信息包括panic value和函数调用的堆栈跟踪信息

Go 语言的接口

并发

goroutine

goroutine 是一种函数的并发执行方式;

func main() {
    go spinner(100 * time.Millisecond)
    const n = 45
    fibN := fib(n) // slow
    fmt.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)
}

channel

channel 用来在 goroutine 之间进行参数传递

import (
	"fmt"
	_ "sync"
	"time"
)

func main() {
	c1 := make(chan string)
	c2 := make(chan string)
	go count(5, "🐑", c1)
	go count(5, "🐑🐑", c2)

	for {
		select {
		case msg := <-c1:
			fmt.Println(msg)
		case msg := <-c2:
			fmt.Println(msg)
		}
	}
}

func count(n int, animal string, c chan string) {
	for i := 0; i < n; i++ {
		c <- animal
		time.Sleep(time.Millisecond * 500)
	}
	close(c)
}

例:在某目录下查找文件名

教程链接

package main

import (
	"fmt"
	"io/ioutil"
	"time"
)

var query = "这里是要搜索的文件名"
var matches int

var worker_num = 0
var max_worker_num = 32
var search_request = make(chan string)
var worker_done = make(chan bool)
var found_match = make(chan bool)

func main() {
	start_time := time.Now()
	go search("D:\\这里路径名太二刺猿了码一下\\", true)
	waitForChannel()
	fmt.Println(matches, "matches")
	fmt.Println(time.Since(start_time))
}

func waitForChannel() {
	for {
		select {
		case path := <-search_request:
			worker_num++
			go search(path, true)
		case <-worker_done:
			worker_num--
			if worker_num == 0 {
				return
			}
		case <-found_match:
			matches++
		}
	}
}

func search(path string, master bool) {
	files, err := ioutil.ReadDir(path)
	if err == nil {
		for _, file := range files {
			name := file.Name()
			if name == query {
				found_match <- true
			}
			if file.IsDir() {
				if worker_num < max_worker_num {
					search_request <- path + name + "\\"
				} else {
					search(path+name+"\\", false)
				}
			}
		}
	}
	if master {
		worker_done <- true
	}
}

互斥锁 sync.Mutex

读写锁 sync.RWMutex

允许多个只读操作并行执行,但写操作会完全互斥。这种锁叫作“多读单写”锁(multiple readers, single writer lock)

延迟初始化 sync.Once