• go 语言基础


    go 语言基础

    package main
    
    import (
    	"fmt"
    	"math/rand"
    )
    
    func main() {
    	fmt.Println("My favorite number is", rand.Intn(10))
    }
    

    每个 Go 程序都是由包组成的。

    程序运行的入口是包 main

    这个程序使用并导入了包 "fmt" 和 "math/rand"

    按照惯例,包名与导入路径的最后一个目录一致。例如,"math/rand" 包由 package rand 语句开始。

    这个代码用圆括号组合了导入,这是“打包”导入语句。

    同样可以编写多个导入语句,例如:

    import "fmt"
    import "math"
    不过使用打包的导入语句是更好的形式。

    在导入了一个包之后,就可以用其导出的名称来调用它。

    在 Go 中,首字母大写的名称是被导出的。

    Foo 和 FOO 都是被导出的名称。名称 foo 是不会被导出的。

    函数

    package main
    
    import "fmt"
    
    func add(x int, y int) int {
    	return x + y
    }
    
    func swap(x, y string) (string, string) {
    	return y, x
    }
    
    func main() {
        a, b := swap("hello", "world")
    	fmt.Println(add(42, 13))
    }
    

    函数可以没有参数或接受多个参数。

    在这个例子中,add 接受两个 int 类型的参数。

    注意类型在变量名 之后

    当两个或多个连续的函数命名参数是同一类型,则除了最后一个类型之外,其他都可以省略。

    在这个例子中 ,

    x int, y int
    被缩写为

    x, y int

    多值返回

    函数可以返回任意数量的返回值。

    swap 函数返回了两个字符串。

    命名返回值

    func split(sum int) (x, y int) {
    	x = sum * 4 / 9
    	y = sum - x
    	return
    }
    

    Go 的返回值可以被命名,并且像变量那样使用。

    返回值的名称应当具有一定的意义,可以作为文档使用。

    没有参数的 return 语句返回结果的当前值。也就是直接返回。

    直接返回语句仅应当用在像下面这样的短函数中。在长的函数中它们会影响代码的可读性。

    变量

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

    var 语句定义了一个变量的列表;跟函数的参数列表一样,类型在后面。

    就像在这个例子中看到的一样,var 语句可以定义在包或函数级别。

    初始化变量

    var c, python, java = true, false, "no!"
    

    变量定义可以包含初始值,每个变量对应一个。

    如果初始化是使用表达式,则可以省略类型;变量从初始值中获得类型。

    短声明变量

    k := 3
    

    在函数中,:= 简洁赋值语句在明确类型的地方,可以用于替代 var 定义。

    函数外的每个语句都必须以关键字开始(varfunc、等等),:= 结构不能使用在函数外。

    基本类型

    Go 的基本类型有Basic types

    bool
    
    string
    
    int  int8  int16  int32  int64
    uint uint8 uint16 uint32 uint64 uintptr
    
    byte // uint8 的别名
    
    rune // int32 的别名
         // 代表一个Unicode码
    
    float32 float64
    
    complex64 complex128
    
    var (
    	ToBe   bool       = false
    	MaxInt uint64     = 1<<64 - 1
    	z      complex128 = cmplx.Sqrt(-5 + 12i)
    )
    

    这个例子演示了具有不同类型的变量。 同时与导入语句一样,变量的定义“打包”在一个语法块中。

    零值

    变量在定义时没有明确的初始化时会赋值为_零值_。

    零值是:

    数值类型为 0
    布尔类型为 false
    字符串为 ""(空字符串)。

    类型转换

    表达式 T(v) 将值 v 转换为类型 T

    一些关于数值的转换:

    var i int = 42
    var f float64 = float64(i)
    var u uint = uint(f)
    

    或者,更加简单的形式:

    i := 42
    f := float64(i)
    u := uint(f)
    

    与 C 不同的是 Go 的在不同类型之间的项目赋值时需要显式转换

    类型推导

    在定义一个变量但不指定其类型时(使用没有类型的 var 或 := 语句), 变量的类型由右值推导得出。

    当右值定义了类型时,新变量的类型与其相同:

    var i int
    j := i // j 也是一个 int
    

    但是当右边包含了未指名类型的数字常量时,新的变量就可能是 int 、 float64 或 complex128。 这取决于常量的精度:

    i := 42           // int
    f := 3.142        // float64
    g := 0.867 + 0.5i // complex128
    

    常量

    const Pi = 3.14
    

    常量的定义与变量类似,只不过使用 const 关键字。

    常量可以是字符、字符串、布尔或数字类型的值。

    常量不能使用 := 语法定义。

    数值常量

    数值常量是高精度的

    一个未指定类型的常量由上下文来决定其类型。

    for

    package main
    
    import "fmt"
    
    func main() {
    	sum := 0
    	for i := 0; i < 10; i++ {
    		sum += i
    	}
    	fmt.Println(sum)
    }
    

    Go 只有一种循环结构——for 循环。

    基本的 for 循环除了没有了 ( ) 之外(甚至强制不能使用它们),看起来跟 C 或者 Java 中做的一样,而 { } 是必须的。

    跟 C 或者 Java 中一样,可以让前置、后置语句为空。

    sum := 1
    	for sum < 1000 {
    		sum += sum
        }
    

    基于此可以省略分号:C 的 while 在 Go 中叫做 for

    if

    if x < 0 {
    		return sqrt(-x) + "i"
        }
    

    if 语句除了没有了 ( ) 之外(甚至强制不能使用它们),看起来跟 C 或者 Java 中的一样,而 { } 是必须的。

    if 的便捷语句

    func pow(x, n, lim float64) float64 {
    	if v := math.Pow(x, n); v < lim {
    		return v
    	}
    	return lim
    }
    

    跟 for 一样,if 语句可以在条件之前执行一个简单的语句。

    由这个语句定义的变量的作用域仅在 if 范围之内。

    if 和 else

    在 if 的便捷语句定义的变量同样可以在任何对应的 else 块中使用。

    switch

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

    一个结构体(struct)就是一个字段的集合。

    除非以 fallthrough 语句结束,否则分支会自动终止

    switch 的执行顺序

    switch 的条件从上到下的执行,当匹配成功的时候停止。

    没有条件的 switch

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

    没有条件的 switch 同 switch true 一样。

    这一构造使得可以用更清晰的形式来编写长的 if-then-else 链。

    defer

    func main() {
    	defer fmt.Println("world")
    
    	fmt.Println("hello")
    }
    

    defer 语句会延迟函数的执行直到上层函数返回。

    延迟调用的参数会立刻生成,但是在上层函数返回前函数都不会被调用。

    defer 栈

    延迟的函数调用被压入一个栈中。当函数返回时, 会按照后进先出的顺序调用被延迟的函数调用。

    指针

    package main
    
    import "fmt"
    
    func main() {
    	i, j := 42, 2701
    
    	p := &i         // point to i
    	fmt.Println(*p) // read i through the pointer
    	*p = 21         // set i through the pointer
    	fmt.Println(i)  // see the new value of i
    
    	p = &j         // point to j
    	*p = *p / 37   // divide j through the pointer
    	fmt.Println(j) // see the new value of j
    }
    

    Go 具有指针。 指针保存了变量的内存地址。

    类型 *T 是指向类型 T 的值的指针。其零值是 nil

    var p *int
    

    & 符号会生成一个指向其作用对象的指针。

    i := 42
    p = &i
    
    • 符号表示指针指向的底层的值。
    fmt.Println(*p) // 通过指针 p 读取 i
    *p = 21         // 通过指针 p 设置 i
    

    这也就是通常所说的“间接引用”或“非直接引用”。

    与 C 不同,Go 没有指针运算。

    结构体

    package main
    
    import "fmt"
    
    type Vertex struct {
    	X int
    	Y int
    }
    
    func main() {
    	fmt.Println(Vertex{1, 2})
    }
    

    一个结构体(struct)就是一个字段的集合。

    (而 type 的含义跟其字面意思相符。)

    结构体字段

    v := Vertex{1, 2}
        v.X = 4
    

    结构体字段使用点号来访问。

    结构体指针

    v := Vertex{1, 2}
    	p := &v
        p.X = 1e9
    

    结构体字段可以通过结构体指针来访问。

    通过指针间接的访问是透明的。

    结构体文法

    v1 = Vertex{1, 2}  // 类型为 Vertex
    v2 = Vertex{X: 1}  // Y:0 被省略
    

    结构体文法表示通过结构体字段的值作为列表来新分配一个结构体。

    使用 Name: 语法可以仅列出部分字段。(字段名的顺序无关。)

    数组

    
    package main
    
    import "fmt"
    
    func main() {
    	var a [2]string
    	a[0] = "Hello"
    	a[1] = "World"
    	fmt.Println(a[0], a[1])
    	fmt.Println(a)
    }
    

    类型 [n]T 是一个有 n 个类型为 T 的值的数组。

    表达式

    var a [10]int
    定义变量 a 是一个有十个整数的数组。

    数组的长度是其类型的一部分,因此数组不能改变大小

    slice

    package main
    
    import "fmt"
    
    func main() {
    	p := []int{2, 3, 5, 7, 11, 13}
    	fmt.Println("p ==", p)
    
    	for i := 0; i < len(p); i++ {
    		fmt.Printf("p[%d] == %d
    ", i, p[i])
    	}
    }
    

    一个 slice 会指向一个序列的值,并且包含了长度信息。

    []T 是一个元素类型为 T 的 slice。

    对 slice 切片

    package main
    
    import "fmt"
    
    func main() {
    	p := []int{2, 3, 5, 7, 11, 13}
    	fmt.Println("p ==", p)
    	fmt.Println("p[1:4] ==", p[1:4])
    
    	// 省略下标代表从 0 开始
    	fmt.Println("p[:3] ==", p[:3])
    
    	// 省略上标代表到 len(s) 结束
    	fmt.Println("p[4:] ==", p[4:])
    }
    

    slice 可以重新切片,创建一个新的 slice 值指向相同的数组。

    表达式

    s[lo:hi]
    

    表示从 lo 到 hi-1 的 slice 元素,左闭右开。因此

    s[lo:lo]
    

    是空的,而

    s[lo:lo+1]
    

    有一个元素。

    构造 slice

    slice 由函数 make 创建。这会分配一个零长度的数组并且返回一个 slice 指向这个数组:

    a := make([]int, 5)  // len(a)=5
    

    为了指定容量,可传递第三个参数到 make

    b := make([]int, 0, 5) // len(b)=0, cap(b)=5
    
    b = b[:cap(b)] // len(b)=5, cap(b)=5
    b = b[1:]      // len(b)=4, cap(b)=4
    

    nil slice

    slice 的零值是 nil

    一个 nil 的 slice 的长度和容量是 0。

    向 slice 添加元素

    向 slice 添加元素是一种常见的操作,因此 Go 提供了一个内建函数 append。 内建函数的文档对 append 有详细介绍。

    func append(s []T, vs ...T) []T
    

    append 的第一个参数 s 是一个类型为 T 的数组,其余类型为 T 的值将会添加到 slice。

    append 的结果是一个包含原 slice 所有元素加上新添加的元素的 slice。

    如果 s 的底层数组太小,而不能容纳所有值时,会分配一个更大的数组。 返回的 slice 会指向这个新分配的数组。

    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
    ", i, v)
    	}
    }
    

    for 循环的 range 格式可以对 slice 或者 map 进行迭代循环

    for _, value := range pow {
    		fmt.Printf("%d
    ", value)
        }
    

    可以通过赋值给 _ 来忽略序号和值。

    如果只需要索引值,去掉“, value”的部分即可。

    map

    package main
    
    import "fmt"
    
    type Vertex struct {
    	Lat, Long float64
    }
    
    var m map[string]Vertex
    
    func main() {
    	m = make(map[string]Vertex)
    	m["Bell Labs"] = Vertex{
    		40.68433, -74.39967,
    	}
    	fmt.Println(m["Bell Labs"])
    }
    

    map 映射键到值。

    map 在使用之前必须用 make 而不是 new 来创建;值为 nil 的 map 是空的,并且不能赋值。

    map 的文法

    package main
    
    import "fmt"
    
    type Vertex struct {
    	Lat, Long float64
    }
    
    var m = map[string]Vertex{
    	"Bell Labs": Vertex{
    		40.68433, -74.39967,
    	},
    	"Google": Vertex{
    		37.42202, -122.08408,
    	},
    }
    
    func main() {
    	fmt.Println(m)
    }
    

    map 的文法跟结构体文法相似,不过必须有键名。

    var m = map[string]Vertex{
    	"Bell Labs": {40.68433, -74.39967},
    	"Google":    {37.42202, -122.08408},
    }
    

    如果顶级的类型只有类型名的话,可以在文法的元素中省略键名。

    修改 map

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

    在 map m 中插入或修改一个元素:

    m[key] = elem
    

    获得元素:

    elem = m[key]
    

    删除元素:

    delete(m, key)
    

    通过双赋值检测某个键存在:

    elem, ok = m[key]
    

    如果 key 在 m 中,ok 为 true 。否则, ok 为 false,并且 elem 是 map 的元素类型的零值。

    同样的,当从 map 中读取某个不存在的键时,结果是 map 的元素类型的零值。

    函数值

    package main
    
    import (
    	"fmt"
    	"math"
    )
    
    func main() {
    	hypot := func(x, y float64) float64 {
    		return math.Sqrt(x*x + y*y)
    	}
    
    	fmt.Println(hypot(3, 4))
    }
    

    函数也是值。

    函数的闭包

    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 函数可以是闭包的。闭包是一个函数值,它来自函数体的外部的变量引用。 函数可以对这个引用值进行访问和赋值;换句话说这个函数被“绑定”在这个变量上。

    例如,函数 adder 返回一个闭包。每个闭包都被绑定到其各自的 sum 变量上。

    方法

    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 没有类。然而,仍然可以在结构体类型上定义方法。

    方法接收者 出现在 func 关键字和方法名之间的参数中。

    type MyFloat float64
    
    func (f MyFloat) Abs() float64 {
    	if f < 0 {
    		return float64(-f)
    	}
    	return float64(f)
    }
    

    你可以对包中的 任意 类型定义任意方法,而不仅仅是针对结构体。

    但是,不能对来自其他包的类型或基础类型定义方法。

    接收者为指针的方法

    package main
    
    import (
    	"fmt"
    	"math"
    )
    
    type Vertex struct {
    	X, Y float64
    }
    
    func (v *Vertex) Scale(f float64) {
    	v.X = v.X * f
    	v.Y = v.Y * f
    }
    
    func (v *Vertex) Abs() float64 {
    	return math.Sqrt(v.X*v.X + v.Y*v.Y)
    }
    
    func main() {
    	v := &Vertex{3, 4}
    	v.Scale(5)
    	fmt.Println(v, v.Abs())
    }
    

    方法可以与命名类型或命名类型的指针关联。

    刚刚看到的两个 Abs 方法。一个是在 *Vertex 指针类型上,而另一个在 MyFloat 值类型上。 有两个原因需要使用指针接收者。首先避免在每个方法调用中拷贝值(如果值类型是大的结构体的话会更有效率)。其次,方法可以修改接收者指向的值。

    尝试修改 Abs 的定义,同时 Scale 方法使用 Vertex 代替 *Vertex 作为接收者。

    当 v 是 Vertex 的时候 Scale 方法没有任何作用。Scale 修改 v。当 v 是一个值(非指针),方法看到的是 Vertex 的副本,并且无法修改原始值。

    Abs 的工作方式是一样的。只不过,仅仅读取 v。所以读取的是原始值(通过指针)还是那个值的副本并没有关系。

    接口

    package main
    
    import (
    	"fmt"
    	"math"
    )
    
    type Abser interface {
    	Abs() float64
    }
    
    func main() {
    	var a Abser
    	f := MyFloat(-math.Sqrt2)
    	v := Vertex{3, 4}
    
    	a = f  // a MyFloat 实现了 Abser
    	a = &v // a *Vertex 实现了 Abser
    
    	// 下面一行,v 是一个 Vertex(而不是 *Vertex)
    	// 所以没有实现 Abser。
    	a = v
    
    	fmt.Println(a.Abs())
    }
    
    type MyFloat float64
    
    func (f MyFloat) Abs() float64 {
    	if f < 0 {
    		return float64(-f)
    	}
    	return float64(f)
    }
    
    type Vertex struct {
    	X, Y float64
    }
    
    func (v *Vertex) Abs() float64 {
    	return math.Sqrt(v.X*v.X + v.Y*v.Y)
    }
    

    接口类型是由一组方法定义的集合。

    接口类型的值可以存放实现这些方法的任何值。

    注意: 例子代码的 22 行存在一个错误。 由于 Abs 只定义在 *Vertex(指针类型) 上, 所以 Vertex(值类型) 不满足 Abser

    隐式接口

    package main
    
    import (
    	"fmt"
    	"os"
    )
    
    type Reader interface {
    	Read(b []byte) (n int, err error)
    }
    
    type Writer interface {
    	Write(b []byte) (n int, err error)
    }
    
    type ReadWriter interface {
    	Reader
    	Writer
    }
    
    func main() {
    	var w Writer
    
    	// os.Stdout 实现了 Writer
    	w = os.Stdout
    
    	fmt.Fprintf(w, "hello, writer
    ")
    }
    

    类型通过实现那些方法来实现接口。 没有显式声明的必要;所以也就没有关键字“implements“。

    隐式接口解藕了实现接口的包和定义接口的包:互不依赖。

    因此,也就无需在每一个实现上增加新的接口名称,这样同时也鼓励了明确的接口定义。

    包 io 定义了 Reader 和 Writer;其实不一定要这么做。

    Stringers

    package main
    
    import "fmt"
    
    type Person struct {
    	Name string
    	Age  int
    }
    
    func (p Person) String() string {
    	return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
    }
    
    func main() {
    	a := Person{"Arthur Dent", 42}
    	z := Person{"Zaphod Beeblebrox", 9001}
    	fmt.Println(a, z)
    }
    

    一个普遍存在的接口是 fmt 包中定义的 Stringer。

    type Stringer struct {
    String() string
    }
    Stringer 是一个可以用字符串描述自己的类型。fmt包 (还有许多其他包)使用这个来进行输出。

    错误

    package main
    
    import (
    	"fmt"
    	"time"
    )
    
    type MyError struct {
    	When time.Time
    	What string
    }
    
    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 程序使用 error 值来表示错误状态。

    与 fmt.Stringer 类似,error 类型是一个内建接口:

    type error interface {
        Error() string
    }
    

    (与 fmt.Stringer 类似,fmt 包在输出时也会试图匹配 error。)

    通常函数会返回一个 error 值,调用的它的代码应当判断这个错误是否等于 nil, 来进行错误处理。

    i, err := strconv.Atoi("42")
    if err != nil {
        fmt.Printf("couldn't convert number: %v
    ", err)
    }
    fmt.Println("Converted integer:", i)
    

    error 为 nil 时表示成功;非 nil 的 error 表示错误。

    Readers

    package main
    
    import (
    	"fmt"
    	"io"
    	"strings"
    )
    
    func main() {
    	r := strings.NewReader("Hello, Reader!")
    
    	b := make([]byte, 8)
    	for {
    		n, err := r.Read(b)
    		fmt.Printf("n = %v err = %v b = %v
    ", n, err, b)
    		fmt.Printf("b[:n] = %q
    ", b[:n])
    		if err == io.EOF {
    			break
    		}
    	}
    }
    

    io 包指定了 io.Reader 接口, 它表示从数据流结尾读取。

    Go 标准库包含了这个接口的许多实现, 包括文件、网络连接、压缩、加密等等。

    io.Reader 接口有一个 Read 方法:

    func (T) Read(b []byte) (n int, err error)
    Read 用数据填充指定的字节 slice,并且返回填充的字节数和错误信息。 在遇到数据流结尾时,返回 io.EOF 错误。

    例子代码创建了一个 strings.Reader。 并且以每次 8 字节的速度读取它的输出。

    Web 服务器

    package main
    
    import (
    	"fmt"
    	"log"
    	"net/http"
    )
    
    type Hello struct{}
    
    func (h Hello) ServeHTTP(
    	w http.ResponseWriter,
    	r *http.Request) {
    	fmt.Fprint(w, "Hello!")
    }
    
    func main() {
    	var h Hello
    	err := http.ListenAndServe("localhost:4000", h)
    	if err != nil {
    		log.Fatal(err)
    	}
    }
    

    包 http 通过任何实现了 http.Handler 的值来响应 HTTP 请求:

    package http

    type Handler interface {
    ServeHTTP(w ResponseWriter, r *Request)
    }
    在这个例子中,类型 Hello 实现了 http.Handler

    访问 http://localhost:4000/ 会看到来自程序的问候。

    图片

    Package image 定义了 Image 接口:

    package image
    
    type Image interface {
        ColorModel() color.Model
        Bounds() Rectangle
        At(x, y int) color.Color
    }
    

    注意Bounds 方法的 Rectangle 返回值实际上是一个 image.Rectangle, 其定义在 image 包中。

    (参阅文档了解全部信息。)

    color.Color 和 color.Model 也是接口,但是通常因为直接使用预定义的实现 image.RGBA 和 image.RGBAModel 而被忽视了。这些接口和类型由image/color 包定义。

    goroutine

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

    goroutine 是由 Go 运行时环境管理的轻量级线程。

    go f(x, y, z)
    开启一个新的 goroutine 执行

    f(x, y, z)
    f , x , y 和 z 是当前 goroutine 中定义的,但是在新的 goroutine 中运行 f

    goroutine 在相同的地址空间中运行,因此访问共享内存必须进行同步。sync 提供了这种可能,不过在 Go 中并不经常用到,因为有其他的办法。(在接下来的内容中会涉及到。)

    channel

    package main
    
    import "fmt"
    
    func sum(a []int, c chan int) {
    	sum := 0
    	for _, v := range a {
    		sum += v
    	}
    	c <- sum // 将和送入 c
    }
    
    func main() {
    	a := []int{7, 2, 8, -9, 4, 0}
    
    	c := make(chan int)
    	go sum(a[:len(a)/2], c)
    	go sum(a[len(a)/2:], c)
    	x, y := <-c, <-c // 从 c 中获取
    
    	fmt.Println(x, y, x+y)
    }
    

    channel 是有类型的管道,可以用 channel 操作符 <- 对其发送或者接收值。

    ch <- v    // 将 v 送入 channel ch。
    v := <-ch  // 从 ch 接收,并且赋值给 v。
    

    (“箭头”就是数据流的方向。)

    和 map 与 slice 一样,channel 使用前必须创建:

    ch := make(chan int)
    

    默认情况下,在另一端准备好之前,发送和接收都会阻塞。这使得 goroutine 可以在没有明确的锁或竞态变量的情况下进行同步。

    缓冲 channel

    package main
    
    import "fmt"
    
    func main() {
    	c := make(chan int, 2)
    	c <- 1
    	c <- 2
    	fmt.Println(<-c)
    	fmt.Println(<-c)
    }
    

    channel 可以是 带缓冲的。为 make 提供第二个参数作为缓冲长度来初始化一个缓冲 channel:

    ch := make(chan int, 100)
    

    向缓冲 channel 发送数据的时候,只有在缓冲区满的时候才会阻塞。当缓冲区清空的时候接受阻塞。

    修改例子使得缓冲区被填满,然后看看会发生什么(会报错)。

    range 和 close

    package main
    
    import (
    	"fmt"
    )
    
    func fibonacci(n int, c chan int) {
    	x, y := 0, 1
    	for i := 0; i < n; i++ {
    		c <- x
    		x, y = y, x+y
    	}
    	close(c)
    }
    
    func main() {
    	c := make(chan int, 10)
    	go fibonacci(cap(c), c)
    	for i := range c {
    		fmt.Println(i)
    	}
    }
    

    发送者可以 close 一个 channel 来表示再没有值会被发送了。接收者可以通过赋值语句的第二参数来测试 channel 是否被关闭:当没有值可以接收并且 channel 已经被关闭,那么经过

    v, ok := <-ch
    

    之后 ok 会被设置为 false

    循环 for i := range c 会不断从 channel 接收值,直到它被关闭。

    注意: 只有发送者才能关闭 channel,而不是接收者。向一个已经关闭的 channel 发送数据会引起 panic。 还要注意: channel 与文件不同;通常情况下无需关闭它们。只有在需要告诉接收者没有更多的数据的时候才有必要进行关闭,例如中断一个 range

    select

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

    select 语句使得一个 goroutine 在多个通讯操作上等待。

    select 会阻塞,直到条件分支中的某个可以继续执行,这时就会执行那个条件分支。当多个都准备好的时候,会随机选择一个。

    默认选择

    package main
    
    import (
    	"fmt"
    	"time"
    )
    
    func main() {
    	tick := time.Tick(100 * time.Millisecond)
    	boom := time.After(500 * time.Millisecond)
    	for {
    		select {
    		case <-tick:
    			fmt.Println("tick.")
    		case <-boom:
    			fmt.Println("BOOM!")
    			return
    		default:
    			fmt.Println("    .")
    			time.Sleep(50 * time.Millisecond)
    		}
    	}
    }
    

    当 select 中的其他条件分支都没有准备好的时候,default 分支会被执行。

    为了非阻塞的发送或者接收,可使用 default 分支:

    select {
    case i := <-c:
        // 使用 i
    default:
        // 从 c 读取会阻塞
    }
    
  • 相关阅读:
    C++ static 几点总结
    Coursera-An Introduction to Interactive Programming in Python (Part 1)-Mini-project #4 —"Pong"
    Coursera-An Introduction to Interactive Programming in Python (Part 1)-Mini-project #3 —"Stopwatch: The Game"
    Coursera-An Introduction to Interactive Programming in Python (Part 1)-Mini-project — “Guess the number” game
    Coursera-An Introduction to Interactive Programming in Python (Part 1)-Mini-project— Rock-paper-scissors-lizard-Spock
    Coursera-C程序设计进阶-编程题#1:单词翻转
    Coursera-C程序设计进阶-编程题#4:Tomorrow never knows?
    多项式加法
    Cordova/Ionic 开发安卓Android如何调试?
    Web Api 之 404 Resource Not Found 错误消息封装
  • 原文地址:https://www.cnblogs.com/ttxs69/p/12580106.html
Copyright © 2020-2023  润新知