go 零散笔记

我从17年开始写go代码,到现在断断续续写了四年有余,其实比较惭愧,目前对go的认识非常浅薄。

究其原因,一个是我使用go的开发只是工作上粘合使用,每年写go代码的时间也不足1个月,基本上是在原有框架上做一些新功能的开发。得益于过去多年在laravel上的经验,看api文档和谷歌能力还是不错的,socket交互、orm、mongodb、k8s client-go等东西上手并不难,看半天基本上也就明白了如何使用。

二个是我工作中心更多是放在k8s这套系统以及偏网络方向上,虽然也是研发,更多是行业和架构层面的。

从今年下半年开始,我的工作重心转到了go的开发上来,而我个人也倾向于使用go作为我未来的主力开发语言。接下来这段时间我会记录更多关于 go 的基础知识。

这篇文章没什么重点,记录一些只言片语吧。

一、学习书籍

当遇到看不懂的内容时,有可能是作者的思考回路和我们的有差别。

不必纠结,跳过去,当看到同样内容不同作者的描述,你可能会豁然开朗。

入门:

入门时要注重理解go的设计理念和语言机制(Language Mechanics),

语言机制包括Go语言的句法、数据结构、解耦。

  • 《Go 程序设计语言》——许式伟译(英文原版翻译的,感觉细看浪费时间,快速浏览/后期针对性溯源就行。)
  • 《Go 语言编程》——许式伟(感觉适合基础入门,以补全理论概念铺垫为主,实战代码可以后期再看)

熟练:

熟练时要理解软件设计,研究并发,Go协程(Goroutine)、数据竞赛、多个channel和不用模式和用模式下的操作

高级:

了解基本单元测试、表测试、自测试等发测试方法,以及常见的标准等,还有各种包(Packages)。

更多选看:

二、Go 概念只言片语

这一部分大多来自 《Go 语言编程》——许式伟

2. 1 一些网站:

2. 2 基础类型:

  • int/unit/string/bool

  • 大整数big.Rat/浮点数fload/复数complex (math包)

  • string (strings包/strconv包/fmt包/utf8包/unicode包/regexp包)

  • 字符类型rune

  • 错误类型error

2. 3 组合类型:

  • 指针

  • 数组

  • slice切片

  • map(哈希表/字典)

  • 通道channel

  • struct 结构体

  • interface接口

    • 指定一组方法,抽象的,不可以实例化。接口的名字,默认以er结尾。接口可以嵌入。

    • 空接口 interface{}, 可以表示任意值,相当于指向任意类型的指针。

2. 4 流程控制:

  • 选择
    • 条件语句 if else
    • 选择语句 switch case/select
      • break continue fallthrough
  • 循环 for 和 range
  • 跳转 goto

2. 5 函数调用:

  • 大小写区分:
    • 小写字母开头的函数只在本包内可见,大写字母开头的函数才能被其他包使用。
    • 这个规则也适用于类型和变量的可见性。
  • 函数可以像普通变量一样被传递或使用

  • 不定参数:

    • func myfunc(args ...int)
    • func Printf(format string, args ...interface{})
  • 多返回值:

    • func (file *File) Read(b []byte) (n int, err Error)
  • 匿名函数/闭包:

    • 闭包内可以引用父方法的变量
    • 父方法为闭包提供绑定的计算环境(作用域)
  • 错误处理:

    • type error interface {
      Error() string
      }
      
    • defer

      • 先进后出的原则,延迟执行,一般用于文件、通道chan关闭、错误处理等。
    • panic/recover
  • init()和main()

image-20210819162920319

  • 动态函数
  • 泛型函数
  • 高阶函数
  • 纯记忆函数

2. 6 类型系统:

image-20210823113029415

2. 7 面向对象:

  • 构造函数,以NewXXX 来命名,表示“构造函数”

    func NewRect(x, y, width, height float64) *Rect {
    	return &Rect{x, y, width, height}
    }
    
  • 有继承,直接在struct引用父struct就ok了(匿名组合)。可以重写覆盖父方法。

  • 方法/变量的可见性,用大小写表示public/private。

  • 接口,隐式声明。

    • 变量类型查询: v1.(type)

2. 8 并发编程:

使用场景:

image-20210823142347960

实现方式:

image-20210823142544454

协程:

image-20210823143544145

goroutine:

  • go Add(1, 1)
    
  • 并发通信

    • 共享数据(c/c++,加锁)
    • 消息通信(go)
  • channel是类型相关的,只能传递一种类型的值,这个类型需要在声明时指定。

    • var chanName chan ElementType // 定义
      c := make(chan int, 1024) // 初始化,带缓冲区
          
      ch <- value // 将一个数据写入(发送)至channel的语法
      value := <-ch // 从channel中读取数据
          
      // 不带缓冲区时,向channel写入数据和读取数据会导致程序阻塞,直到有其他goroutine从这个channel中读取数据/写入数据为止。
          
      close(ch) //关闭channel
      
    • 单向channel:

      // 基于 ch4 ,通过类型转换初始化了两个单向channel:单向读的 ch5 和单向写的 ch6 。
          
      ch4 := make(chan int)
      ch5 := <-chan int(ch4) // ch5就是一个单向的读取channel
      ch6 := chan<- int(ch4) // ch6 是一个单向的写入channel
      
    • demo:

      image-20210823144823672

    • 高阶用法:

      • 底层多核并行:还没支持?

      • 出让时间片:Gosched()

      • 同步锁:sync.Mutex 、 sync.RWMutex

      • 多次只运行一次:once.Do()

      • 代码示例:

        image-20210823162559527

2. 9 网络编程:

  • Socket
  • http
  • rpc
  • json
  • net/http包

2. 10 安全编程:

  • pki
  • hash函数
    • MD5
    • SHA-1
  • https

2.11 代码规范:

  • 任何需要对外暴露的名字必须以大写字母开头、不需要对外暴露的则应该以小写字母开头

  • Go语言明确宣告了拥护骆驼命名法而排斥下划线法

  • 代码块中,左花括号{ 必须跟在同一行

  • 工程结构:

    README
    LICENSE
    bin/
    pkg/
    src/
    

2. 12 工程构建与命令行:

命令行主要完成以下这几类工作:

  • 代码格式化

  • 代码质量分析和修复

  • 单元测试与性能测试

    • 创建以_test结尾的go文件,形如[^.]*_test.go

    • 以 Test 和 Benchmark 为函数名前缀并以 *testing.T 为单一参数的函数。

      func TestAdd1(t *testing.T)
      func BenchmarkAdd1(t *testing.T)
      
  • 工程构建

  • 代码文档的提取和展示

  • 跨平台开发、编译

2. 13 高阶话题

  • 反射(reflection)

  • 多语言

    • cgo
  • 协程goroutine原理

  • 标准库

    go 标准库,导入使用unix风格。导入包的使用惯例,pkg.item

    https://books.studygolang.com/The-Golang-Standard-Library-by-Example/

    输入输出 (Input/Output)
    文本
    数据结构与算法
    日期与时间
    数学计算
    文件系统
    数据持久存储与交换
    数据压缩与归档
    测试
    进程、线程与 goroutine
    网络通信与互联网 (Internet)
    email
    应用构建 与 debug
    运行时特性
    底层库介绍
    同步
    加解密
    

2. 14 其它

方法与函数的区别:

函数是指不属于任何结构体、类型的方法,也就是说函数是没有接收者的;

方法是有接收者的,我们说的方法要么是属于一个结构体的,要么属于一个新定义的类型的。

方法在定义的时候,会在func和方法名之间增加一个参数,这个参数就是接收者,这样我们定义的这个方法就和接收者绑定在了一起,称之为这个接收者的方法。

type person struct {
	name string
}
 
func (p person) String() string{
	return "the person name is "+p.name
}

用户自定义类型,也应该实现Len()和Cap()方法。

Go语言的符号(symbol)一样,以大写字母开头的常量/函数在包外可见。

方法func

常量const(字面量)

变量var,赋予一内存块名字,该内存块保存特定的数据类型。可以匿名(返回值,等号左侧填_)

指针:保存了另一个变量内存地址的变量。

& ,取址操作符。

*,解引用操作符。

如果一个函数/方法返回超过4/5个值,最好使用一个切片/指向结构体的指针来传递,成本较低。

三、Go 实操笔记

这一部分大多来自:《Go 语言学习笔记》——雨痕

  • 运行时runtime、编译时Combile-time。

    • 静态类型语言都需要编译,Go是静态类型语言,不能在运行时改变变量类型。
  • 变量

    • 使用关键字 var 定义变量,自动初始化为零值。
    • 在函数内部,可用更简略的 “:=” 方式定义变量。
    • 可一次定义多个变量。
    • 特殊只写变量 “_“,用于忽略值占位。
  • 常量

    • 必须是编译期可确定的数字、字符串、布尔值。

    • 如不提供类型和初始化值,那么值与上一常量相同。

    • iota是常量计数器,在定义枚举时很有用。

      type AudioOutput int
          
      const (
          OutMute AudioOutput = iota // 0
          OutMono                    // 1
          OutStereo                  // 2
          _
          _
          OutSurround                // 5
      )
      
  • 引用类型

    • 包括 slice、map 和 channel。
    • 内置函数 new 计算类型大小,为其分配零值内存,返回指针。
    • make 会被编译器翻译成具体的创建函数,由其分配内存和初始化成员结构,返回对象而非指针。
  • 字符串

    • 字符串是不可变值类型,内部用指针指向 UTF-8 字节数组。

    • 默认值是空字符串 ““。

    • 用索引号访问某字节,如 s[i]。

    • 不能用索引号获取字节元素指针,&s[i] 非法。

    • 不可变类型,无法修改字节数组。

    • 字节数组尾部不包含 NULL。

    • 使用 “`” 定义不做转义处理的原始字符串,支支持跨行行。

    • 连接跨行字符串时,”+” 必须在上一行末尾,否则编译错误。

    • 支持用两个索引号返回子串。子串依然指向原字节数组,仅修改了指针和长度属性。

      s := "Hello, World!"
      s1 := s[:5] // Hello
      s2 := s[7:] // World!
      s3 := s[1:5] // ello
      
    • rune 是 int32 的别名,几乎在所有方面等同于int32,用于区分字符值和整数值。

    • golang 中的字符有两种,uint8(byte)代表ASCII的一个字符,rune代表一个utf-8字符。

    • 修改字符串,可先将其转换成 []rune 或 []byte,完成后再转换为 string.无论哪种转换,都会重新分配内存,并复制字节数组。有汉字等需要utf8支持的就用rune,没汉字随意。

      func main() {
          s := "abc汉字"
          for i := 0; i < len(s); i++ {
              // byte
              fmt.Printf("%c,", s[i])
          }
          fmt.Println()
          for _, r := range s {
              // rune
              fmt.Printf("%c,", r)
          }
      }
      
  • 指针

    • 默认值 nil,没有 NULL 常量。

    • 操作符 “&” 取变量地址,”*” 透过指针访问⺫目目标对象。

    • 不支持指针运算,不支持 “->” 运算符,直接用 “.” 访问目标成员。

      func main() {
          type data struct{ a int }
          var d = data{1234}
          var p *data
              
          p = &d
          
          fmt.Printf("%p, %v\n", p, p.a)    // 直接用指针访问目标对象成员,无须转换。
      }
      
    • 可以在 unsafe.Pointer 和任意类型指针间进行转换。

      func main() {
          x := 0x12345678
          p := unsafe.Pointer(&x) // *int -> Pointer
          n := (*[4]byte)(p) 		// Pointer -> *[4]byte
              
          for i := 0; i < len(n); i++ {
              fmt.Printf("%X ", n[i])
          }
      }
          
      // 78 56 34 12
      
    • 将 Pointer 转换成 uintptr,可变相实现指针运算。

      func main() {
          d := struct {
              s string
              x int
          }{"abc", 100}
              
          p := uintptr(unsafe.Pointer(&d)) // *struct -> Pointer -> uintptr
          p += unsafe.Offsetof(d.x) // uintptr + offset
              
          p2 := unsafe.Pointer(p) // uintptr -> Pointer
          px := (*int)(p2) // Pointer -> *int
          *px = 200 // d.x = 200
              
          fmt.Printf("%#v\n", d)
      }
          
      // struct { s string; x int }{s:"abc", x:200}
      
  • 自定义类型

    • 可用 type 在全局或函数内定义新类型。 type bigint int64
    • 显示转换。x := 1234 var b bigint = bigint(x)
  • 保留字

    break default func interface select
    case defer go map struct
    chan else goto package switch
    const fallthrough if range type
    continue for import return var
    
  • 位运算

  • 循环

    • for,支持初始化语句。

      s := "abcd"
      for i, n := 0, length(s); i < n; i++ {
          println(i, s[i])
      }
      
    • range,range 会复制对象。

      for k, v := range m {
      	println(k, v)
      }
      
    • switch,可省略break,表达式可以任意类型,不限于常量,需要继续下一支支,使用 fallthrough

    • 省略条件表达式,switch可当 if…else if…else 使用

      switch i := x[2]; {
          // 带初始化语句
          case i > 0:
          println("a")
          case i < 0:
          println("b")
          default:
          println("c")
      }
      
    • break 可用用于 for、switch、select,而 continue 仅能用于 for 循环。

    • 支持在函数内 goto 跳转。标签名区分大小写

  • 函数

    • 不支持 嵌套 (nested)、重载 (overload) 和 默认参数 (default parameter)。

    • 无需声明原型 支持不定长变参 支持多返回值 支持命名返回参数 支持匿名函数和闭包

    • 有返回值的函数,必须有明确的终止语句,否则会引发编译错误。

    • 变参,本质上就是 slice。只能有一个,且必须是最后一个。

    • 使用 slice 对象做变参时,必须展开。

      func main() {
          s := []int{1, 2, 3}
          println(test("sum: %d", s...))
      }
      
    • 多返回值可直接作为其他函数调用实参。

    • 使用用 slice 对象做变参时,必须展开。

    • 命名返回参数可被同名局部变量遮蔽,此时需要显式返回。

    • 匿名函数可赋值给变量,做为结构字段,或者在 channel 里传送。

    • 延迟执行defer

      • 在 defer 归属的函数即将返回时,将延迟处理的语句按 defer 的逆序进行执行

        func test() {
            defer func() {
                fmt.Println(recover())
            }()
                  
            defer func() {
                panic("defer panic")
            }()
                  
            panic("test panic")
        }
        func main() {
            test()
        }
        
      • 滥用 defer 可能会导致性能问题,尤其是在一个 “大循环” 里。

      • 如果需要保护代码片段,可将代码块重构成匿名函数,如此可确保后续代码被执行。

  • 数组

    • 数组是值类型,赋值和传参会复制整个数组,而不是指针。

    • 内置函数 len 和 cap 都返回数组长度 (元素数量)。

    • 指针数组 [n]*T,数组指针 *[n]T。

    • 值拷贝会造成性能问题,请使用 slice,或数组指针。

      a := [3]int{1, 2} // 未初始化元素值为 0。
      b := [...]int{1, 2, 3, 4} // 通过初始化值确定数组⻓长度。
      c := [5]int{2: 100, 4:200} // 使用用索引号初始化元素。
      
  • Slice

    • 初始化和数组很像,不用声明长度

      s1 := []int{0, 1, 2, 3, 8: 100} // 通过初始化表达式构造,可使用用索引号。
      fmt.Println(s1, len(s1), cap(s1))
          
      s2 := make([]int, 6, 8) // 使用用 make 创建,指定 len 和 cap 值。
      fmt.Println(s2, len(s2), cap(s2))
          
          
      s3 := make([]int, 6) // 省略 cap,相当于 cap = len。
      fmt.Println(s3, len(s3), cap(s3))
      
    • 可用指针直接访问底层数组,退化成普通数组操作。

      s := []int{0, 1, 2, 3}
      p := &s[2] // *int, 获取底层数组元素指针。
      *p += 100
      fmt.Println(s)  // [0 1 102 3]
      
    • 基于已有 slice 创建新 slice 对象,新对象依旧指向原底层数组。

      s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
      s1 := s[2:5]  // [2 3 4]
      s1[2] = 100
          
      s2 := s1[2:6]  // [100 5 6 7]
      s2[3] = 200
          
      fmt.Println(s) // [0 1 2 3 100 5 6 200 8 9]
      
    • append/copy

  • map 哈希表

    • 初始化

      m := make(map[string]int, 1000)
          
      m := map[string]int{
      "a": 1,
      }
          
      if v, ok := m["a"]; ok {
          // 判断 key 是否存在。
          println(v)
      }
          
      println(m["c"]) // 对于不存在的 key,直接返回 \0,不会出错。
      m["b"] = 2 // 新增或修改。
      delete(m, "c") // 删除。如果 key 不存在,不会出错。
      println(len(m)) // 获取键值对数量。cap 无无效。
          
      for k, v := range m { // 迭代,可仅返回 key。随机顺序返回,每次都不相同。
          println(k, v)
      }
      
    • 从 map 中取回的是一个 value 临时复制品,对其成员的修改是没有任何意义的。正确做法是完整替换 value 或使用用指针。

      type user struct{ name string }
      m := map[int]user{
          1: {"user1"},
          // 当 map 因扩张而而重新哈希时,各键值项存储位置都会发生生改变。 因此,map
          // 被设计成 not addressable。 类似 m[1].name 这种期望透过原 value
      } // 指针修改成员的行行为自自然会被禁止止。
          
      m[1].name = "Tom" // Error: cannot assign to m[1].name
          
      u := m[1]
      u.name = "Tom"
      m[1] = u      // 替换 value。
          
      m2 := map[int]*user{
      	1: &user{"user1"},
      }
      m2[1].name = "Jack" // 返回的是指针复制品。透过指针修改原对象是允许的。
      
  • struct

    • 支持指向自身类型的指针成员。

    • 顺序初始化必须包含全部字段,否则会出错。

      type Node struct {
          _ int
          id int
          data *byte
          next *Node
      }
      
    • 匿名字段

      可以像普通字段那样访问匿名字段成员, 编译器从外向内逐级查找所有层次的匿名字段,直到发现目标或出错。

      type User struct {
          name string
      }
      type Manager struct {
          User
          title string
      }
      

      外层同名字段会遮蔽嵌入入字段成员,解决方法是使用用显式字段名。

      不能同时嵌入入某一类型和其指针类型,因为它们名字相同。

  • 面向对象

    • 面向对象三大大特征里里, Go 仅支持封装(匿名字段的内存布局和行为类似继承)。没有class 关键字,没有继承、多态等等。
  • 方法

    • 方法总是绑定对象实例,并隐式将实例作为第一实参 (receiver)。

    • 只能为当前包内命名类型定义方法。 参数 receiver 可任意命名。如方法中未曾使用,可省略参数名。 参数 receiver 类型可以是 T 或 *T。基类型 T 不能是接口或指针。 不支持方法重载,receiver 只是参数签名的组成部分。 可用用实例 value 或 pointer 调用用全部方法,编译器自动转换。

    • 没有构造和析构方法,通常用简单工厂模式返回对象实例。

      type Queue struct {
          elements []interface{}
      }
          
      func NewQueue() *Queue {
          // 创建对象实例。
          return &Queue{make([]interface{}, 10)}
      }
          
      func (*Queue) Push(e interface{}) error {
          // 省略 receiver 参数名。
          panic("not implemented")
      }
          
      // func (Queue) Push(e int) error {
          // panic("not implemented")
          // Error: method redeclared: Queue.Push
              
      // }
      func (self *Queue) length() int {
          // receiver 参数名可以是 self、this 或其他。
          return len(self.elements)
      }
      
    • 不支持多级指针查找方法成员。

    • 通过匿名字段,可获得和继承类似的复用能力。依据编译器查找次序,只需在外层定义同名方法,就可以实现 “override”。

      type User struct {
          id
          int
          name string
      }
      type Manager struct {
          User
          title string
      }
          
      func (self *User) ToString() string {
          return fmt.Sprintf("User: %p, %v", self, self)
      }
      func (self *Manager) ToString() string {
          return fmt.Sprintf("Manager: %p, %v", self, self)
      }
      func main() {
          m := Manager{User{1, "Tom"}, "Administrator"}
          fmt.Println(m.ToString())
          fmt.Println(m.User.ToString())
      }
      
  • Interface

    • 接口命名习惯以 er 结尾,结构体。 接口只有方法签名,没有实现。 接口没有数据字段。 可在接口中嵌入其他接口。

    • 空接口 interface{} 没有任何方法签名,也就意味着任何类型都实现了空接口。其作用类似面向对象语言言中的根对象 object。

      type Stringer interface {
          String() string
      }
      type Printer interface {
          Stringer     // 接口口嵌入入。
          Print()
      }
      type User struct {
          id    int
          name string
      }
          
      func (self *User) String() string {
      	return fmt.Sprintf("user %d, %s", self.id, self.name)
      }
      func (self *User) Print() {
      	fmt.Println(self.String())
      }
          
      func main() {
          var t Printer = &User{1, "Tom"}     // *User 方方法集包含 String、Print。
          t.Print()
      }
      
    • 接口对象由接口表 (interface table) 指针和数据指针组成。 只有 tab 和 data 都为 nil 时,接口才等于 nil。

      var a interface{} = nil // tab = nil, data = nil
      var b interface{} = (*int)(nil) // tab 包含 *int 类型信息, data = nil
          
      type iface struct {
          itab, data uintptr
      }
          
      ia := *(*iface)(unsafe.Pointer(&a))
      ib := *(*iface)(unsafe.Pointer(&b))
          
      fmt.Println(a == nil, ia)
      fmt.Println(b == nil, ib, reflect.ValueOf(b).IsNil())
          
      //
      // true {0 0}
      // false {505728 0} true
      

      这个特性,官方有个关于error的有趣的描述:https://golang.org/doc/faq#nil_error,简单来说就是不要自己定义error,免得判断 nil 时候出问题。这里还有个类似的例子:

      image-20210825130630230

    • 数据指针持有的是目标对象的只读复制品,复制完整对象或指针。

      type User struct {
          id    int
          name string
      }
      func main() {
          u := User{1, "Tom"}
          var i interface{} = u
          u.id = 2
          u.name = "Jack"
          fmt.Printf("%v\n", u)
          fmt.Printf("%v\n", i.(User))
      }
          
      // {2 Jack}
      // {1 Tom}
      
    • 接口转型返回临时对象,只有使用指针才能修改其状态。

      type User struct {
          id    int
          name string
      }
      func main() {
          u := User{1, "Tom"}
          var vi, pi interface{} = u, &u
              
          // vi.(User).name = "Jack"    // Error: cannot assign to vi.(User).name
          pi.(*User).name = "Jack"
              
          fmt.Printf("%v\n", vi.(User))
          fmt.Printf("%v\n", pi.(*User))
      }
      
    • 接口类型判断

      type User struct {
          id int
          name string
      }
          
      func (self *User) String() string {
      	return fmt.Sprintf("%d, %s", self.id, self.name)
      }
      func main() {
          var o interface{} = &User{1, "Tom"}
          if i, ok := o.(fmt.Stringer); ok {       // ok-idiom
              fmt.Println(i)
          }
          u := o.(*User)
          // u := o.(User)     // panic: interface is *main.User, not main.User
          fmt.Println(u)
      }
      

      批量判断:

      func main() {
          var o interface{} = &User{1, "Tom"}
          switch v := o.(type) {
              case nil:
                  // o == nil
                  fmt.Println("nil")
              case fmt.Stringer:
                  // interface
                  fmt.Println(v)
              case func() string:
                  // func
                  fmt.Println(v())
              case *User:
                  // *struct
                  fmt.Printf("%d, %s\n", v.id, v.name)
              default:
                  fmt.Println("unknown")
          }
      }
      
    • 让编译器检查,以确保某个类型实现接口。

      var _ fmt.Stringer = (*Data)(nil)
      
  • 并发goroutine

    • 入口函数 main 就以 goroutine 运行。另有与之配套的 channel 类型,实现 “以通讯来共享内存” 的 CSP 模式。

      go func() {
      	println("Hello, World!")
      }()
      
    • 调度器不能保证多个 goroutine 执行行次序,且进程退出时不会等待它们结束。

    • 默认情况下,进程启动后仅允许一个系统线程服务于 goroutine。可使用环境变量或标准库函数 runtime.GOMAXPROCS 修改,让调度器用多个线程实现多核并行,而不仅仅是并发。

    • 调用 runtime.Goexit 将立即终止当前 goroutine 执行,调度器确保所有已注册 defer延迟调用被执行。

  • channel

    • 简单使用

      var chanName chan ElementType // 定义
      c := make(chan int, 1024) // 初始化,带缓冲区
          
      ch <- value // 将一个数据写入(发送)至channel的语法
      value := <-ch // 从channel中读取数据
          
      // 不带缓冲区时,向channel写入数据和读取数据会导致程序阻塞,直到有其他goroutine从这个channel中读取数据/写入数据为止。
          
      close(ch) //关闭channel
      
    • 默认为同步模式,需要发送和接收配对。否则会被阻塞。

    • 异步方式通过判断缓冲区来决定是否阻塞。如果缓冲区已满,发送被阻塞;缓冲区为空,接收被阻塞。

    • 异步 channel 可减少排队阻塞,具备更高的效率。

      func main() {
          data := make(chan int) // 数据交换队列
          exit := make(chan bool) // 退出通知
          go func() {
              for d := range data {            // 从队列迭代接收数据,直到 close 。
                  fmt.Println(d)
              }
                  
              fmt.Println("recv over.")
              exit <- true        // 发出退出通知。
          }()
              
          data <- 1    // 发送数据。
          data <- 2
          data <- 3
          close(data)    // 关闭队列。
              
              
          fmt.Println("send over.")
          <-exit     // 等待退出通知。
      }
      
    • channel 应该考虑使用指针规避大大对象拷贝,将多个元素打包,减小缓冲区大小等。

    • 除用 range 外,还可用 ok-idiom 模式判断 channel 是否关闭。

      for {
          if d, ok := <-data; ok {
              fmt.Println(d)
          } else {
              break
          }
      }
      
    • 单向 chan, 不能将单向 channel 转换为普通 channel。

      c := make(chan int, 3)
      var send chan<- int = c // send-only
      var recv <-chan int = c // receive-only
          
      send <- 1  发送数据
      // <-send  // Error: receive from send-only type chan<- int
          
      <-recv
      // recv <- 2 // Error: send to receive-only type <-chan int
          
          
      
    • 在循环中使用 select default case 需要小心,避免形成洪水。

  • 工具

    • go build

    • go install

    • go clean

    • go get

    • go tool objdump

    • 跨平台编译

    • 数据竞争 (data race)

    • go test

    • Benchmark

    • PProf

四、hello world

main.go

package main

import "fmt"

func main() {
  fmt.Printf("hello, world\n")
}

初始化go module

go mod init kelu.org/apptest
go mod tidy
go build 

这样会生成一个 apptest 的可执行文件。


《十四五规划和2035年远景》的一些摘抄 我常用的 vscode 快捷键