hello world 开始

package main

import "fmt"

func main() {
    fmt.Println("Hello, 世界")
}

  • 每个 Go 程序都是由包构成的(类似面向对象中的命名空间),按照约定,包名和导入路径最后一个元素一致。例如,"math/rand" 包中的源码均以 package rand 语句开始。

1. 导入包

1.1 第一种方式

import "fmt"
import "math"

1.2 第二种方式

import (
  "fmt"
  "math"
)

2. 导出名

  • 在 Go 中,一个名字以大写字母开头,那么它是已导出的(包外可以访问包中函数或变量),未以大写字母开头就是未导出的(包外不可访问包中的函数和变量,仅包内可访问)。

函数

  • 函数是 Go 中可重复利用的代码块

函数的声明:

func add (a int, b int) int {

}
func/*func声明函数的关键字*/ add/*函数名*/ (a int, b int/*形参列表*/) int/*返回值参数类型*/ {
  /* 代码块 */
}

函数可以多值返回

命名返回值

Go 的返回值可被命名,它们会被视作定义在函数顶部的变量。返回时用 没有参数的return语句 返回已命名的返回值。

package main

import "fmt"

/* 命名返回,注意 return 和 返回值参数列表 */
func split(sum int) (x, y int) {
    x = sum * 4 / 9
    y = sum - x
    return
}

func main() {
    fmt.Println(split(17))
}

变量

变量声明

var 语句用于声明一个变量列表,var 可以出现在包或函数级别。

package main

import "fmt"

var c, python, java bool

func main() {
    var i int
    fmt.Println(i, c, python, java)
}

短变量声明

在函数中可以使用 := 替代 var 声明, := 短声明不能再函数外使用

package main

import "fmt"

func main() {
    var i, j int = 1, 2
    k := 3
    c, python, java := true, false, "no!"

    fmt.Println(i, j, k, c, python, java)
}

变量初始化

给声明的变量赋初始值

package main

import "fmt"

var i, j int = 1, 2

func main() {
    var c, python, java = true, false, "no!"
    fmt.Println(i, j, c, python, java)
}

Go 基本类型

bool 布尔类型

  • 取值 真(true) 或 假(false),常用于逻辑判断。

int 整数类型

  • int 带符号的, 32 位机器是 32 位宽,64位机器上是 64 位宽
  • uint 不带符号的, 32 位机器是 32 位宽, 64位机器上是 64 位宽
  • int8 带符号固定 8 位宽
  • uint8 不带符号固定 8 位宽
  • int16 带符号,固定 16 位宽
  • uint16 不带符号,固定 16 位宽
  • int32 带符号,固定 32 位宽
  • uint32 不带符号,固定 32 位宽
  • int64 带符号,固定 64 位宽
  • uint64 不带符号,固定 64 位宽
  • uintptr 32 位系统上是 32 位宽,64 位系统上是 64 位宽

  • 补充:

    1. 带符号,可以表示正整数 和 负整数; 不带符号,只可以表示正整数。
    2. 所说的带宽是指二进制位数。
    3. 位宽表示取值范围,假设位宽为 8,则可以表示带符号数的范围是: -(2 ^ 7) 到 +(2 ^ 7 - 1),为什么正数部分减一?因为还有一个 0 需要表示;为什么是 2 ^ 7 ?因为八位带宽中,有一位是符号位(就是表示正数或者负数),真正表示数字部分的只有7位;可以表示的不带符号数的返回时: 0 到 2 ^ 8 。其它位宽算法类似。
    4. 为什么 uintptr 带宽只和操作系统cpu位数有关?因为 uintptr 表示内存地址,64位操作系统的地址总线带宽是 64(或者说64根线总称为地址总线),那么它的寻址范围是:02 ^ 64。显然只跟硬件有关。

byte 类型

  • int8 的别名

rune 类型

  • int32 的别名
  • 表示一个 unicode 码(go使用unicode编码,解决非英文字符编码问题)。

string 类型

  • 字符串类型

float 类型

  • float32:由 32 位带宽表示的浮点型(带小数部分的数字),也称单精度浮点型。
  • float64:由 64 位带宽表示的浮点型,也称双精度浮点型。

complex 复数类型

  • complex64
  • complex128

零值

  • 没有明确初始值的变量声明会被赋予它们的零值。
  • 数值类型的零值为 0,
  • 布尔类型的零值为 false,
  • 字符串的零值为 ""(空字符串)。

类型转换

  • go 中类型转换必须是显式转换
  • 转换方式:表达式T(v) 将值 v 转换为类型T。
  • 数值型的类型转换大带宽转为小带宽时会丢失精度
  • 这种转换仅仅适用于数值类型之间的转换(不包含复数)

常量

  • const 关键字声明常量
  • 常量可以是字符、字符串、布尔、数值
  • 常量声明不能用 :=

循环

for 循环

  • for 循环由三部分组成:初始化语句、条件表达式、后置语句,这三部分用分号隔开。
  • 初始化语句:设置循环条件的初始化值
  • 条件表达式:每次循环体执行前检查条件表达式是否成立,成立才执行循环体
  • 后置语句: 改变循环初始值
  • 初始化语句 和 后置语句 可以省略(可替代)
package main

import "fmt"

/* for 循环 */
func main() {
    sum := 0
    for i := 0/*初始化语句*/; i < 10/*条件表达式*/; i++/*后置语句*/ {
    /* 循环体 */
        sum += i
    }
    fmt.Println(sum)
}

go 中的"while" 循环

  • for 也是 Go 中的 while循环
package main

import "fmt"

func main() {
    sum := 1
    for sum < 1000 {
        sum += sum
    }
    fmt.Println(sum)
}

无限循环

  • 省略条件判断就是无限循环
package main

func main() {
    for {
    }
}

条件判断 if

if 语句示例

package main

import (
    "fmt"
    "math"
)

func sqrt(x float64) string {
    if x < 0 {
        return sqrt(-x) + "i"
    }
    return fmt.Sprint(math.Sqrt(x))
}

func main() {
    fmt.Println(sqrt(2), sqrt(-4))
}

if 简短语句

  • if 语句可以在条件表达式前执行一个简单的语句。该语句声明的变量作用域仅在 if 之内。
package main

import (
    "fmt"
    "math"
)

func pow(x, n, lim float64) float64 {
    if v := math.Pow(x, n); v < lim {
    /* v 这个变量只能在现在这个作用域内 或 if 的其它判断作用域(比如:else 或 else if)内使用 */
        return v
    }
    return lim
}

func main() {
    fmt.Println(
        pow(3, 2, 10),
        pow(3, 3, 20),
    )
}

if 和 else

选择/判断 switch

  • switch 就是一连串 if - else 语句
  • switch 的结果与 case 匹配了就会执行 case 之后的语句
  • 没有任何匹配时,执行 default 语句后的内容
  • go 的 switch 与 C/C++ 的switch不同,go 中switch只执行匹配的case之后就自动break出去了(go中 switch 也可以和c/c++中的一样)
  • go 中 switch 中的 case 不必每整数 或 常量

switch示例

package main

import (
    "fmt"
    "runtime"
)

func main() {
    fmt.Print("Go runs on ")
    switch os := runtime.GOOS; os {
    case "darwin":
        fmt.Println("OS X.")
    case "linux":
        fmt.Println("Linux.")
    default:
        // freebsd, openbsd,
        // plan9, windows...
        fmt.Printf("%s.", os)
    }
}

switch 求值顺序

  • switch 的 case 语句从上到下顺次执行,知道匹配成功时停止,之后的 case 语句不再执行(当然没有匹配上的 case 也不会执行)

没有条件的 switch

  • 这完全就是 if - then - else 的另一种写法
package main

import (
    "fmt"
    "time"
)

func main() {
    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.")
    }
}

defer

  • defer 语句会将函数推迟到外层函数返回之后执行。推迟调用的函数,其参数会立即求值,但直到外层函数返回前,该函数都不会被调用。
  • 注意,主要是 defer 后函数的调用时间(外层函数返回后) + 函数参数(立即求值)
package main

import (
    "fmt"
    "time"
)

func main() {
    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.")
    }
}

defer 栈

  • 推迟的函数调用会被压入一个栈中,当外层函数返回时,被推迟的函数会按后进先出的顺序调用
package main

import "fmt"

func main() {
    fmt.Println("counting")

    for i := 0; i < 10; i++ {
        defer fmt.Println(i)
    }

    fmt.Println("done")
}

指针

  • 指针,用来保存值的内存地址
  • *T 表示指向类型 T 的指针,指针的默认零值是 nil
  • & 操作符会对变量取其指针(也就是变量存放的地址)
    • 操作符会对指针取值
  • go 中没有指针运算(这是与 C/C++ 最大的不同,意味着指针步长这一重要的指针参数对开发人员来说变得透明)

结构体 struct

  • 结构体是一个 字段的集合

结构体访问成员变量

  • 结构体指针,对于指向结构体的指针p,在go中,既可以通过 (*p).X 来访问结构体中的字段 X,也可以通过 p.X来访问。后者称 隐式间接引用

结构体初始化

  • 可以通过直接列出字段的值来新分配一个结构体
package main

import "fmt"

type Vertex struct {
    X, Y int
}

var (
    v1 = Vertex{1, 2}  // 初始化 vertex 中的 X = 1, Y = 2
    v2 = Vertex{X: 1}  // 初始化 vertex 中的 X = 1, Y = 0
    v3 = Vertex{}      // 初始化 vertex 中的 X = 0, Y = 0
    p  = &Vertex{1, 2} // 获得 *Vertex 类型 p (也就是 Vertex 类型的指针 p)
)

func main() {
    fmt.Println(v1, p, v2, v3)
}

数组

  • 类型 [n]T 表示拥有 n 个 T 类型值的数组
  • 数组长度是固定的(因为数组在内存上占用一段连续的内存空间)

切片

  • 类型 []T 表示一个元素类型为 T 的切片(注意声明时候和数组的不同之处)
  • 切片并不存储任何数据,切片与一个底层数组对应,对切片的修改结果会反映到对底层数组中。
  • 切片通过两个下标来界定元素,a[low : heigh],表示切片中 下标是 low 到 下标是 heigh - 1 之间的所有元素
package main

import "fmt"

func main() {
    primes := [6]int{2, 3, 5, 7, 11, 13}

    var s []int = primes[1:4]
    fmt.Println(s)
}

切片默认行为

  • 切片默认下界值我 0, 上界 值为改切片的长度
  • 对于数组 var a [10]int 来说,以下切片是等价的:
a[0:10]
a[:10]
a[0:]
a[:]

切片的长度和容量

  • 切片长度:切片包含元素个数 len()。
  • 切片容量:切片第一个元素开始,到底层数组元素末尾的个数 cap()。
  • 切片的零值:切片的零值是 nil, 表示切片的长度和容量都为0,且没有底层数组。

make 创建切片

  • a := make([]int, 5) 表示包含 5 个元素,这屋个元素值为 int 类型的零值。
  • a := make([]int, 5 10) 表示包含 5 个元素为int的零值,最多可存 10 个int元素, len(a) == 5 cap(a) == 10
  • 当用 make 创建切片 cap(a) == 10 的时候,若想访问第 11 个元素,则会报错。

切片的切片

  • 切片可以包含任何数据类型,包括它自己。
package main

import (
    "fmt"
    "strings"
)

func main() {
    // Create a tic-tac-toe board.
    board := [][]string{
        []string{"_", "_", "_"},
        []string{"_", "_", "_"},
        []string{"_", "_", "_"},
    }

    // The players take turns.
    board[0][0] = "X"
    board[2][2] = "O"
    board[1][2] = "X"
    board[1][0] = "O"
    board[0][2] = "X"

    for i := 0; i < len(board); i++ {
        fmt.Printf("%s\n", strings.Join(board[i], " "))
    }
}

切片追加元素

  • go 内建 append 函数追加新元素到切片
func append(s []T, vs ...T) []T

切片的遍历

  • for 循环的 range 形式可遍历切片或映射。当使用 for 循环遍历切片时,每次迭代都会返回两个值。第一个值为当前元素的下标,第二个值为该下标所对应元素的一份副本。
  • 使用 range 时可以将下标或值赋予 _ 来忽略它。
package main

import "fmt"

var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}

func main() {
    for i, v := range pow {
        fmt.Printf("2**%d = %d\n", i, v)
    }
}

映射

  • 映射将键映射到值。映射的零值为 nil 。nil 映射既没有键,也不能添加键。
  • 映射必须有键名

修改映射

  • 在映射 m 中插入或修改元素: m[key] = elem
  • 获取元素 ele = m[key],当key不存在时候,获得的 ele 是零值
  • 删除元素 delete(m, key)
  • 通过双赋值检测某个键是否存在 elem,ok = m[key], 存在ok == true; 否则 ok == false, ok为false时,elem是该映射元素的零值。
package main

import "fmt"

func main() {
    m := make(map[string]int)

    m["Answer"] = 42
    fmt.Println("The value:", m["Answer"])

    m["Answer"] = 48
    fmt.Println("The value:", m["Answer"])

    delete(m, "Answer")
    fmt.Println("The value:", m["Answer"])

    v, ok := m["Answer"]
    fmt.Println("The value:", v, "Present?", ok)
}

函数值

  • 函数也是值,它们可以像其它值一样传递,函数值可以用作函数的参数或返回值
package main

import (
    "fmt"
    "math"
)

func compute(fn func(float64, float64) float64) float64 {
    return fn(3, 4)
}

func main() {
    hypot := func(x, y float64) float64 {
        return math.Sqrt(x*x + y*y)
    }
    fmt.Println(hypot(5, 12))

    fmt.Println(compute(hypot))
    fmt.Println(compute(math.Pow))
}

函数的闭包

  • go函数可以是一个闭包,闭包是一个函数值,它引用了其函数体之外的变量。该函数可以访问并赋予其引用的变量的值。换句话说,该函数被“绑定”在了这些变量上。
package main

import "fmt"

func adder() func(int) int {
    sum := 0
    return func(x int) int {
        sum += x
        return sum
    }
}

func main() {
    pos, neg := adder(), adder()
    for i := 0; i < 10; i++ {
        fmt.Println(
            pos(i),
            neg(-2*i),
        )
    }
}

方法

  • go 没有类,但是可以为结构体类型定义方法
  • 方法就是一类带特殊的 接受者 参数的函数,方法接受者在它自己的参数列表内,位于 func 关键字和方法名之间
  • 方法就是函数,只是方法带了接受者参数。
  • 接收者的类型定义和方法声明必须在同一包内;不能为内建类型声明方法。
  • 接受者:指针接受者 和 值接受者,因为时常需要修改接受者的值,所以一般用指针接受者,值接受者不能修改接收者的值(其实接受到的是一份拷贝数据)。
package main

import (
    "fmt"
    "math"
)

type Vertex struct {
    X, Y float64
}

func (v Vertex) Abs() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
    v := Vertex{3, 4}
    fmt.Println(v.Abs())
}

方法与指针重定向

  • 函数传参:带指针阐述的函数必须接受一个指针
  • 函数传参:接受一个值作为参数的函数必须接受一个指定类型的值

  • 以指针为接收者的方法调用时,接收者既能是值,又能是指针
  • 以值为接收者的方法被调用的时候,接收者既能是值,又能是指针

  • 方法的上述特殊行为是因为 Go 进行了指针重定向。

  • 注意:字段中的值能不能由方法进行修改,完全看方法定义的时候接收者是指针还是值,只要接收者是指针,无论方法调用的时候是以指针调用还是值调用,都可以修改方法中的值。
  • 当不允许方法修改字段值的时候,可以考虑值接收者,但是这样在参数传递时候会发生深拷贝,耗用时间;所有尽量还是一直都用指针接受者比较好。

接口

  • 接口类型是由一组方法签名定义的集合,接口类型的变量可以保存任何实现了这些方法的值。

接口与隐式实现

  • 某类型通过一个接口的所有方法来实现该接口
  • 隐式接口从接口的实现中解耦了定义,这样接口的实现就可以出现在任何包中,无需提前准备。
package main

import "fmt"

type I interface {
    M()
}

type T struct {
    S string
}

// 此方法表示类型 T 实现了接口 I,但我们无需显式声明此事。
func (t T) M() {
    fmt.Println(t.S)
}

func main() {
    var i I = T{"hello"}
    i.M()
}

接口值

  • 在内部,接口值可以看做包含值 和 具体类型的元组 (value, type),接口保存了一个具体底层类型的具体值。接口值调用方法时会执行其底层类型的同名方法。
package main

import (
    "fmt"
    "math"
)

/* 接口定义 */
type I interface {
    M()
}

type T struct {
    S string
}

/* *T 类型实现了接口 I */
func (t *T) M() {
    fmt.Println(t.S)
}

type F float64

/* F类型实现了接口 I */
func (f F) M() {
    fmt.Println(f)
}

func main() {
  var i I

  i = &T{"Hello"}
  describe(i)
  i.M()

  i = F(math.Pi)
  describe(i)
  i.M()
}

func describe(i I) {
  /* %v打印相应值的Go语法表示, %T 响应值的类型的Go语法表示 */
  fmt.Printf("(%v, %T)\n", i, i)
}

底层为 nil 的接口值

  • 即便接口内的具体值为 nil,方法仍然会被 nil 接收者调用。
  • 保存了 nil 具体值的接口其自身并不为 nil。这与 C/C++ 中不同, C/C++ 中无论在哪 NULL 就是 0,具体到指针就是指空指针,而go具体接口是分配了内存空间,保存了nil。
package main

import "fmt"

type I interface {
    M()
}

type T struct {
    S string
}

func (t *T) M() {
    if t == nil {
        fmt.Println("<nil>")
        return
    }
    fmt.Println(t.S)
}

func main() {
    var i I

    var t *T // t 值为 nil,
    i = t
    describe(i)
    i.M()

    i = &T{"hello"} //
    describe(i)
    i.M()
}

func describe(i I) {
    fmt.Printf("(%v, %T)\n", i, i)
}

nil 接口值

  • nil 接口值既不保存值也不保存具体类型。为 nil 接口调用方法会产生运行时错误,因为接口的元组内并未包含能够指明该调用哪个 具体 方法的类型。

空接口

  • 指定了 0 个方法的接口值被称为空接口 interface{}
  • 空接口可以保存任何类型的值。因为每个类型都至少实现了零个方法。
  • 空接口被用来处理未知类型的值

类型断言

  • 类型断言提供了访问接口底层具体值的方式
  • t := i.(T) 该语句断言接口值 i 保存了具体类型T,并将其底层类型为 T 的值赋予变量 t。若 i 并未保存 T 类型的值,该语句就会触发一个 恐慌。
  • 为了 判断 一个接口值是否保存了一个特定的类型,类型断言可返回两个值:其底层值以及一个报告断言是否成功的布尔值。t, ok := i.(T)若 i 保存了一个 T,那么 t 将会是其底层值,而 ok 为 true。否则,ok 将为 false 而 t 将为 T 类型的零值,程序并不会产生恐慌。
package main

import "fmt"

func main() {
    var i interface{} = "hello"

    s := i.(string)
    fmt.Println(s)

    s, ok := i.(string)
    fmt.Println(s, ok)

    f, ok := i.(float64)
    fmt.Println(f, ok)

    f = i.(float64) // panic
    fmt.Println(f)
}

类型选择

  • 类型选择 是一种按顺序从几个类型断言中选择分支的结构。
  • 类型选择与一般的 switch 语句相似,不过类型选择中的 case 为类型(而非值), 它们针对给定接口值所存储的值的类型进行比较。
switch v := i.(type) {
case T:
    // v 的类型为 T
case S:
    // v 的类型为 S
default:
    // 没有匹配,v 与 i 的类型相同
}

类型选择中的声明与类型断言 i.(T) 的语法相同,只是具体类型 T 被替换成了关键字 type。

package main

import "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)
}

内建接口 -- Stringer

  • Stringer 是一个可以用字符串描述自己的类型。fmt 包(还有很多包)都通过此接口来打印值。
type Stringer interface {
    String() string
}

内建接口 -- error (错误)

  • Go 程序使用 error 值来表示错误状态
  • error 是一个内建的接口
type error interface {
    Error() string
}
  • 自定义错误
package main

import (
    "fmt"
    "time"
)

/* 类型 */
type MyError struct {
    When time.Time
    What string
}

/* 实现 error 接口的 Error 方法 */
func (e *MyError) Error() string {
    return fmt.Sprintf("at %v, %s", e.When, e.What)
}

func run() error {
    return &MyError{time.Now(),"it didn't work"}
}

func main() {
    if err := run(); err != nil {
        fmt.Println(err)
    }
}

协程(go)

  • 协程 是由 Go 运行时管理的轻量级线程。
  • 协程 在相同的地址空间中运行,因此在访问共享的内存时必须进行同步。sync 或者 chan 可以实现。
package main

import (
    "fmt"
    "time"
)

func say(s string) {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s)
    }
}

func main() {
    go say("world")
    say("hello")
}

信道

  • 信道是带有类型的管道,你可以通过它用信道操作符 <- 来发送或者接收值。
  • 默认情况下,信道的发送和接收操作在另一端准备好之前都会阻塞。这使得 Go 程可以在没有显式的锁或竞态变量的情况下进行同步。
ch <- v    // 将 v 发送至信道 ch。
v := <-ch  // 从 ch 接收值并赋予 v。

信道创建

  • ch := make(chan int)

带缓冲的信道

  • 信道可以是 带缓冲的。将缓冲长度作为第二个参数提供给 make 来初始化一个带缓冲的信道:
  • 仅当信道的缓冲区填满后,向其发送数据时才会阻塞。当缓冲区为空时,接受方会阻塞。
  • ch := make(chan int, 100)
package main

import "fmt"

func main() {
    ch := make(chan int, 3)  // 信道缓冲小于 2 会崩溃。因为写阻塞的原因
    ch <- 1
    ch <- 2
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}

管道接收数据

  1. 第一种读取v, ok := <- ch: 管道中没有数据且已关闭,则ok == false
  2. 第二种读取 for i := range {}

close

  • 发送者可以通过 close 关闭一个信道,表示没有需要发送的值了。
  • 注意,只有发送者可以关闭信道,接受者不可以;向一个已关闭的信道发送数据会引发恐慌。
  • 即使发送者关闭了管道,只要管道内有数据,接受者依然可以正常接收。
package main

import (
    "time"
    "fmt"
)

func main() {
    c := make(chan int, 1000)

    go func(){
        for i := 0; i <= 100; i++ {
            c <- i
        }
        close(c)
    }()


    for i := range c {
        fmt.Println(i)
        time.Sleep(time.Second)
    }
}

select 语句

  • select 语句使一个 Go 协程可以等待多个通信操作
  • select 会阻塞到某个分支可以继续执行为止,这时就会执行该分支。当多个分支都准备好时会随机选择一个执行。
  • 当 select 中的其它分支都没有准备好时,default 分支就会执行。为了在尝试发送或者接收时不发生阻塞,可使用 default 分支:
package main

import "fmt"

func fibonacci(c, quit chan int) {
    x, y := 0, 1
    for {
        select {
        case c <- x:
            x, y = y, x+y
        case <-quit:
            fmt.Println("quit")
            return
        }
    }
}

func main() {
    c := make(chan int)
    quit := make(chan int)
    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(<-c)
        }
        quit <- 0
    }()
    fibonacci(c, quit)
}

互斥锁(sync.Mutex)

  • 当多个协程访问共享变量的时候使用。
package main

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

// SafeCounter 的并发使用是安全的。
type SafeCounter struct {
    v   map[string]int
    mux sync.Mutex
}

// Inc 增加给定 key 的计数器的值。
func (c *SafeCounter) Inc(key string) {
    c.mux.Lock()
    // Lock 之后同一时刻只有一个 goroutine 能访问 c.v
    c.v[key]++
    c.mux.Unlock()
}

// Value 返回给定 key 的计数器的当前值。
func (c *SafeCounter) Value(key string) int {
    c.mux.Lock()
    // Lock 之后同一时刻只有一个 goroutine 能访问 c.v
    defer c.mux.Unlock()
    return c.v[key]
}

func main() {
    c := SafeCounter{v: make(map[string]int)}
    for i := 0; i < 1000; i++ {
        go c.Inc("somekey")
    }

    time.Sleep(time.Second)
    fmt.Println(c.Value("somekey"))
}

最后

  • 因为工作方向不同,理解问题重点也会有所偏重。对于一门语言需要反复使用才能逐渐理解,这个教程我看过3遍了,每次都有新的理解。
  • 原文来自: go 指南