Go语言笔记如何高效学习?

2026-05-17 01:001阅读0评论SEO基础
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计8080个文字,预计阅读时间需要33分钟。

Go语言笔记如何高效学习?

变量声明+go语言中局部变量声明后必须使用,否则报错。全局变量无此限制+整数、浮点数、string等基本类型为值类型,赋值后变量名指向各自独立的值;而复杂类型为引用类型,两个变量引用同一内存地址。

变量声明
  • go语言中局部变量声明后必须被使用,否则报错。全局变量无此限制
  • 整数、浮点数、string这些基本类型为值类型,赋值后变量名指向各自的值;而复杂类型为引用类型,两变量赋值后指向同一内存区域
  • 变量声明时若不赋值则为系统默认值(数值类型0, 布尔false, string为"",其他为nil)

// 一般变量声明的3种方法 // 第一种: var 变量名 类型 var i int // 第二种(省略类型,由初值自动判断): var 变量名 var i = 5 //第三种(省略类型和var关键字,但只能用于函数体内),如: str := "string" // := 声明符前面必须有一个变量之前未被声明,而不能单纯用来给已声明变量赋值,否则报错 // 多变量声明 var i, j = 5, 7 // 三种方法均可 i, j = j, i // swap values var ( // 这种因式分解关键字的写法一般用于声明全局变量 a int b bool ) // 空白标识符_(实际上无法访问值,用于丢弃函数的某个返回值) _, d = foo()

常量声明:

// 格式:const identifier [type] = value const f, s = 5.5, "hello" //const可用于枚举,缺省时将使用上一行的表达式 // iota为系统特殊变量,每次遇到const时置0, 每定义一个变量值加1 const( a = iota //0 b //1 c //2 d = "ha" //独立值,iota += 1 e //"ha" iota += 1 f = 100 //iota +=1 g //100 iota +=1 h = iota //7,恢复计数 i //8 ) 语言结构

  1. 一行只有一条语句时可省略分号

// 当前程序的包名 package main // 导入其他包 import . "fmt" // 导入所有函数和变量(不建议) import _ "fmt" // 仅导入,运行init函数,不导入内部变量和函数 // 常量定义 const identifier [type] = value const PI = 3.14 // 全局变量的声明和赋值 var name = "gopher" // 一般类型声明 type newType int // 结构的声明 type gopher struct{} // 接口的声明 type golang interface{} // 由main函数作为程序入口点启动 func main() { Println("Hello World!") } 运算符

  1. 算术:+ - * / % ++(自增) --(自减)
  2. 关系:> >= < <= == !=
  3. 逻辑:&& || !
  4. 位:& | ^ << >>^ 做一元运算符为位取反,二元为异或)
  5. 赋值运算符 := 以及算术、位运算符结合=的赋值运算符
  6. 其他:&(取地址运算符)、* (取对应地址值)
优先级 运算符 5 * / % << >> & &^ 4 + - | ^ 3 == != < <= > >= 2 && 1 || 数组
  1. 声明和初始化

    var array [5]int // 未初始化默认零值 array := [...]int{1, 2, 3} // “...” 自动确定大小 array := [5]int{1: 1, 3: 5} // 初始化arr[1] = 1, arr[3] = 5 array := [5]*int{1: new(int), 3: new(int)}

  2. 数组采用值传递,需满足大小和类型相同才能赋值。若需要在函数中修改,用数组指针传参

    func main() { array := [5]int{1, 2, 3} modify(&array) } func modify(arr *[5]int) { // 注意区别于指针数组类型 [5]*int arr[1] = 5 }

切片
  1. 定义&初始化

    // 定义 slice := make([]int, 5) // len = cap = 5 slice := make([]int, 5, 10) // len = 5, cap = 10 slice := []int{1:3, 3:5} // 基于现有数组或切片创建,指向原有(底层)数组,相当于引用赋值。取值范围[i, j]代表区间[i,j),此时长度为 len-i, 容量为 cap-i。i,j可省略,此时分别代表0,len // 还可加第三个参数,即[i:j:k], k代表切片容量容量右区间(k<=原cap) slice1 := slice[:] slice2 : slice[:5] // 切片清空 (比如清空缓存) buf = buf[:0] var slice []int // nil切片,指向底层数组的指针为nil slice := []int{} // 空切片,指向底层数组的指针为一个地址

  2. 底层采用数组存储,包含3个字段:

    • 指向底层数组的指针
    • 切片长度
    • 切片容量(仅用于扩展容量,超出长度的部分不能用于索引)
  3. 可通过内置函数 len()cap() 获取切片长度和容量

  4. 赋值为引用传递,此时对切片元素的修改会影响原切片。可用内置的append()对赋值的变量追加元素,若此时底层数组容量足够,则直接对元素进行覆盖(但原切片len和cap不变)。否则新建一个底层数组.

  5. 数组和切片迭代

    for i := 0; i < len(slice); i++ {} for idx, val := range slice {} // idx 可改成用下划线“_”省略

Map映射
  1. 定义&初始化

    // make dict := make(map[string]int) // 字面量初始化 dict := map[string]int{"张三": 38, "李四": 20} dict := map[string]int{} // 空map var dict map[string]int // nil映射,无空间,使用前需用make赋值 dict = make(map[string]int) dict["张三"] = 38

  2. 取值 & 删除

    age := dict["张三"] // 若键不存在,返回零值 age, exists := dict["张三"] // exists 布尔类型,表示值是否存在 delete(dict, "张三") // 若map中存在对应key,删除之

  3. 赋值采用引用传递

  4. Map存储的是无序键值对的组合,遍历结果不确定。需要有序结果的话需要先排序

    // 遍历方式 for key := range dict {} for key, val := range dict {} var names []string for name := range dict { names = append(names, name) } sort.Strings(names) // 排序 for _, key := range names { fmt.Println(key, dict[key]) }

  5. map的键类型可以是任意值类型,内置 or 结构类型,必须能用 == 比较。而map、slice、函数、及含有切片的结构类型等引用类型则不能作为键

结构类型
  1. 基础类型:包括整型、浮点型、字符型、布尔型, 赋值采用值传递。对它们的操作一般返回一个新创建的值,所以是线程安全的。

  2. 引用类型:包括map、slice、chan、函数类型、接口类型,采用引用传递,本质是传递了底层的指针值(注意slice结构中还包含了len和cap两个基本类型,不会在函数中被修改)

  3. 结构类型

    • 采用值传递,但结构体内的引用类型采用引用传递(本质是指针的值传递)
    • 若要修改结构体值,应传递指针

    type person struct{ name string age int } var p person // 声明并初始化变量,默认零值 p := person{"Jack", 5} // 初始化顺序和声明的相同 p := person{age: 5, name: "Jack"} // 不按顺序 // 在函数中修改值 func main() { jim := person{"Jim",10} fmt.Println(jim) modify(&jim) fmt.Println(jim) } func modify(p *person) { p.age =p.age+10 // golang中一律用句号“.”访问成员 }

  4. 自定义类型:定义结构体 或 基于已有类型

    • Go是强类型语言。即使底层类型相同,如果类型名不同也不能相互赋值

      type Duration int64 var dur Duration dur = int64(100) // Error fmt.Println(dur)

    • 基于已有类型重新定义类型的好处

      • 添加方法
      • 明确业务含义
函数&方法
  1. 函数

    • 函数定义中没有接收者
  2. 方法

    • 方法定义中有接收者

      type person struct{ name string age int } func (p person) String() {} // 值类型接收者 func (p *person) modify() {} // 指针型接收者 // 调用时不必严格采用对应的值或指针的类型,编译器或自动进行类型转换 // 比如var mu sync.Mutex调用mu.Lock()函数 var p person p.modify() (&p).modify() // 两种均可

  3. 大小写

    • 函数、方法、变量名、类型的首字母若大写,则可在包外使用,相当于java的public关键字
  4. 多值返回

    func add(a, b int) (int, error) { return a + b, nil // 返回顺序与声明顺序一致 } func main() { sum, err := add(1, 2) if err != nil { log.Fatal(err) return } fmt.Println(sum) }

  5. 可变参数

    • 函数入参表类型前加 ... ,必须是最后一个入参。 函数内该参数相当于一个数组

    func main() { print("1","2","3") } func print (a ...interface{}){ for _,v:=range a{ fmt.Print(v) } fmt.Println() }

接口
  1. 接口是抽象的,仅包含一组接口方法。具体实现由用户实现

  2. 如果用户定义的类型(定义为 实体类型),实现了接口类型声明的所有方法,那么这个用户定义的类型就实现了这个接口

  3. 多态:实体类型对象可以赋值给对应的接口类型对象。那么在调用接口类型对象的方法时,将转化成对具体实体类型方法的调用

  4. 接口类型被赋值后,包含两个指针。第一个指针指向存储的实体类型的信息和相关联的方法集,第二个指针指向存储的实体类型的值

  5. 方法集

    • 值接收 的 方法,实体类型的值和指针都可以实现对应的接口(指针传递不会被改变值);而若采用 指针接收, 只有实体类型的指针能够实现对应的接口

      func main() { var c cat //值作为参数传递 invoke(&c) // 虽然以指针传递,但不会在方法中改变值 } //需要一个animal接口作为参数 func invoke(a animal){ a.printInfo() } type animal interface { printInfo() } type cat int //值接收者实现animal接口 func (c cat) printInfo(){ fmt.Println("a cat") }

    • Methods Receivers Values (t T) T and *T (t *T) *T
嵌入类型
  1. 示例

    func main() { ad := admin{user{"张三","zhangsan@flysnow.org"},"管理员"} // 必须按定义的方式初始化 ad.user.sayHello() // 被覆盖的方法依然存在 ad.sayHello() invoke(ad) fmt.Println(ad.name) // user成员同时变成admin成员 } type user struct { name string email string } type admin struct { user // 将已声明的结构体嵌入 level string } func (u user) sayHello(){ fmt.Println("Hello,i am a user") } func (a admin) sayHello(){ // 方法重写override fmt.Println("Hello,i am a admin") } type Hello interface { hi() } func (u user) hi() { // user实现了Hello接口,则admin也实现了该接口 fmt.Println("Hi. I'm a user.") } func invoke(person Hello) { person.hi() }

  2. Go语言中没有继承的概念。Go提倡的代码组合方式是组合。嵌入的为内部类型,包含的为外部类型

  3. 性质

    • 嵌入后,内部类型的成员便也成为外部类型的成员
    • 在外部类型中可以对内部类型的方法进行重写。但内部类型中的方法依然存在,可以通过内部类型去调用
    • 如果内部实现了某个接口,则外部类型也实现了该接口
标识符可见性
  1. 示例

    package common import "fmt" func NewLoginer() Loginer{ // 内部设计改变时,不影响用户调用的接口 return defaultLogin(0) } type Loginer interface { Login() } type defaultLogin int func (d defaultLogin) Login(){ fmt.Println("login in...") }

    // -------------- main包 -----------// package main func main() { l:=common.NewLoginer() l.Login() }

  2. 范围:变量、函数、类型、成员(变量/函数)

  3. 通过首字母大小写定义可见性,大写exported,小写unexported

  4. .操作符前面的部分导出了,.操作符后面的部分才有可能被访问;如果.前面的部分都没有导出,那么即使.后面的部分是导出的,也无法访问

    例子 可否访问 Admin.User.Name 是 Admin.User.name 否 Admin.user.Name 否 Admin.user.name 否
Goroutine
  1. 基本概念

    概念 说明 进程 一个程序对应的一个独立程序空间 线程 一个执行空间,一个进程可以有多个线程 逻辑处理器 执行创建的goroutine,绑定一个线程 调度器 Go运行时中的,分配goroutine给不同的逻辑处理器 全局运行队列 所有刚创建的goroutine都会放到这里 本地运行队列 逻辑处理器的goroutine队列
  2. go语言中并发指的是让某个函数独立于其他函数运行的能力,一个goroutine就是一个独立的工作单元,Go的runtime(运行时)会在逻辑处理器上调度这些goroutine来运行,一个逻辑处理器绑定一个操作系统线程,所以说goroutine不是线程,它是一个协程,也是这个原因,它是由Go语言运行时本身的算法实现的

  3. 当我们创建一个goroutine的后,会先存放在全局运行队列中,等待Go运行时的调度器进行调度,把他们分配给其中的一个逻辑处理器,并放到这个逻辑处理器对应的本地运行队列中,最终等着被逻辑处理器执行即可

  4. 设置逻辑处理器个数等于物理核数

    runtime.GOMAXPROCS(runtime.NumCPU())

资源竞争 (data race)
  1. 当两个或多个goroutine在没有相互同步的情况下访问某个共享的资源时,就会出现资源竞争(Data Race)

    var ( count int32 wg sync.WaitGroup ) func main() { wg.Add(2) go incCount() go incCount() wg.Wait() fmt.Println(count) // 结果不确定 } func incCount() { defer wg.Done() for i := 0; i < 2; i++ { value := count runtime.Gosched() value++ count = value } }

  2. 检查data race的方式

    go run -race main.go go build -race // then run the binary generated

  3. 避免并发的方式

    // import "sync.atomic" atomic.AddInt32(&count, 1) // import "sync" var mu sync.Mutex // 互斥锁 mu.Lock() mu.Unlock() // 使用channel

通道Channel
  1. 定义 & 基本使用

    // 双向通道 ch := make(chan int) // 无缓冲通道(同步通道) ch := make(chan int, 5) // 缓冲通道,缓冲容量为5 var ch chan int // nil通道 // 单向通道,仅向通道发送 chan<- int, 仅从通道接收 <-chan int。虽然也能用make新建,但是一个不能填充数据(发送)只能读取的通道是毫无意义的 var ch2 <-chan int var ch2 chan<- int ch2 = ch // 将双向通道赋值给单向通道 ch <- 5 <- ch2 // 获取通道容量和通道内的元素个数 n := len(ch) m := cap(ch) // 关闭通道,此时往channel发送会panic,从channel接收返回零值 close(ch) val, ok := <-ch // ok 表示channel是否已关闭

  2. 无缓冲通道要求发送和接收端都需要做好准备,否则一方将阻塞,等待另一方处理,所以又称同步通道。缓冲通道发送后并不要求立马接收

  3. 只有双向通道才能关闭,且只能关闭一次。重复关闭channel或往关闭了的channel发送数据会panic

并发示例
  1. 并发示例Runner

    package common import ( "errors" "os" "os/signal" "time" ) var ErrTimeOut = errors.New("执行者执行超时") var ErrInterrupt = errors.New("执行者被中断") //一个执行者,可以执行任何任务,但是这些任务是限制完成的, //该执行者可以通过发送终止信号终止它 type Runner struct { tasks []func(int) //要执行的任务 complete chan error //用于通知任务全部完成 timeout <-chan time.Time //这些任务在多久内完成 interrupt chan os.Signal //可以控制强制终止的信号 } func New(tm time.Duration) *Runner { return &Runner{ complete: make(chan error), timeout: time.After(tm), interrupt: make(chan os.Signal, 1), } } //将需要执行的任务,添加到Runner里 func (r *Runner) Add(tasks ...func(int)) { r.tasks = append(r.tasks, tasks...) } //执行任务,执行的过程中接收到中断信号时,返回中断错误 //如果任务全部执行完,还没有接收到中断信号,则返回nil func (r *Runner) run() error { for id, task := range r.tasks { if r.isInterrupt() { return ErrInterrupt } task(id) } return nil } //检查是否接收到了中断信号 func (r *Runner) isInterrupt() bool { select { case <-r.interrupt: signal.Stop(r.interrupt) return true default: return false } } //开始执行所有任务,并且监视通道事件 func (r *Runner) Start() error { //希望接收哪些系统信号 signal.Notify(r.interrupt, os.Interrupt) go func() { r.complete <- r.run() }() select { case err := <-r.complete: return err case <-r.timeout: return ErrTimeOut } }

    package main import ( "flysnow.org/hello/common" "log" "time" "os" ) func main() { log.Println("...开始执行任务...") timeout := 3 * time.Second r := common.New(timeout) r.Add(createTask(), createTask(), createTask()) if err := r.Start(); err != nil{ switch err { case common.ErrTimeOut: log.Println(err) os.Exit(1) case common.ErrInterrupt: log.Println(err) os.Exit(2) } } log.Println("...任务执行结束...") } func createTask() func(int) { return func(id int) { log.Printf("正在执行任务%d", id) time.Sleep(time.Duration(id)* time.Second) } }

  2. 资源共享池 -> Pool

    package common import ( "errors" "io" "sync" "log" ) //一个安全的资源池,被管理的资源必须都实现io.Close接口 type Pool struct { m sync.Mutex res chan io.Closer factory func() (io.Closer, error) closed bool } var ErrPoolClosed = errors.New("资源池已经被关闭。") //创建一个资源池 func New(fn func() (io.Closer, error), size uint) (*Pool, error) { if size <= 0 { return nil, errors.New("size的值太小了。") } return &Pool{ factory: fn, res: make(chan io.Closer, size) }, nil } //从资源池里获取一个资源 func (p *Pool) Acquire() (io.Closer,error) { select { case r,ok := <-p.res: log.Println("Acquire:共享资源") if !ok { return nil,ErrPoolClosed } return r,nil default: log.Println("Acquire:新生成资源") return p.factory() } } //关闭资源池,释放资源 func (p *Pool) Close() { p.m.Lock() defer p.m.Unlock() if p.closed { return } p.closed = true //关闭通道,不让写入了 close(p.res) //关闭通道里的资源 for r:=range p.res { r.Close() } } func (p *Pool) Release(r io.Closer){ //保证该操作和Close方法的操作是安全的 p.m.Lock() defer p.m.Unlock() //资源池都关闭了,就剩这一个没有释放的资源了,释放即可 if p.closed { r.Close() return } select { case p.res <- r: log.Println("资源释放到池子里了") default: log.Println("资源池满了,释放这个资源吧") r.Close() } }

    package main import ( "flysnow.org/hello/common" "io" "log" "math/rand" "sync" "sync/atomic" "time" ) const ( //模拟的最大goroutine maxGoroutine = 5 //资源池的大小 poolRes = 2 ) func main() { //等待任务完成 var wg sync.WaitGroup wg.Add(maxGoroutine) p, err := common.New(createConnection, poolRes) if err != nil { log.Println(err) return } //模拟好几个goroutine同时使用资源池查询数据 for query := 0; query < maxGoroutine; query++ { go func(q int) { dbQuery(q, p) wg.Done() }(query) } wg.Wait() log.Println("开始关闭资源池") p.Close() } //模拟数据库查询 func dbQuery(query int, pool *common.Pool) { conn, err := pool.Acquire() if err != nil { log.Println(err) return } defer pool.Release(conn) //模拟查询 time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond) log.Printf("第%d个查询,使用的是ID为%d的数据库连接", query, conn.(*dbConnection).ID) } //数据库连接 type dbConnection struct { ID int32//连接的标志 } //实现io.Closer接口 func (db *dbConnection) Close() error { log.Println("关闭连接", db.ID) return nil } var idCounter int32 //生成数据库连接的方法,以供资源池使用 func createConnection() (io.Closer, error) { //并发安全,给数据库连接生成唯一标志 id := atomic.AddInt32(&idCounter, 1) return &dbConnection{id}, nil }

  3. 补充:原生资源池 sync.Pool

    • 资源池大小默认无上限
    • 缓存的对象是临时的,在下一次GC时将会被清除
    • Get() 从资源池获取资源,Put() 放入资源池,返回任意对象 interface{}

    package main import ( "log" "math/rand" "sync" "sync/atomic" "time" ) const ( //模拟的最大goroutine maxGoroutine = 5 ) func main() { //等待任务完成 var wg sync.WaitGroup wg.Add(maxGoroutine) p:=&sync.Pool{ // 通过字面量声明 New: createConnection, } //模拟好几个goroutine同时使用资源池查询数据 for query := 0; query < maxGoroutine; query++ { go func(q int) { dbQuery(q, p) wg.Done() }(query) } wg.Wait() } //模拟数据库查询 func dbQuery(query int, pool *sync.Pool) { conn:=pool.Get().(*dbConnection) defer pool.Put(conn) //模拟查询 time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond) log.Printf("第%d个查询,使用的是ID为%d的数据库连接", query, conn.ID) } //数据库连接 type dbConnection struct { ID int32//连接的标志 } //实现io.Closer接口 func (db *dbConnection) Close() error { log.Println("关闭连接", db.ID) return nil } var idCounter int32 //生成数据库连接的方法,以供资源池使用 func createConnection() interface{} { //并发安全,给数据库连接生成唯一标志 id := atomic.AddInt32(&idCounter, 1) return &dbConnection{ID:id} }

读写锁
  1. 原因:读 - 读不互斥,读 - 写、写 - 写操作才互斥。直接采用sync.Mutex效率较低

    Go语言笔记如何高效学习?

  2. 示例

    var mu sync.RWMutex // 读锁 mu.RLock() mu.RUnlock() // 写锁 mu.Lock() mu.Unlock()

  3. 示例 SynchronizedMap

    package common import ( "sync" ) //安全的Map type SynchronizedMap struct { rw *sync.RWMutex data map[interface{}]interface{} } //存储操作 func (sm *SynchronizedMap) Put(k,v interface{}){ sm.rw.Lock() defer sm.rw.Unlock() sm.data[k]=v } //获取操作 func (sm *SynchronizedMap) Get(k interface{}) interface{}{ sm.rw.RLock() defer sm.rw.RUnlock() return sm.data[k] } //删除操作 func (sm *SynchronizedMap) Delete(k interface{}) { sm.rw.Lock() defer sm.rw.Unlock() delete(sm.data,k) } //遍历Map,并且把遍历的值给回调函数,可以让调用者控制做任何事情 func (sm *SynchronizedMap) Each(cb func (interface{},interface{})){ sm.rw.RLock() defer sm.rw.RUnlock() for k, v := range sm.data { cb(k,v) } } //生成初始化一个SynchronizedMap func NewSynchronizedMap() *SynchronizedMap{ return &SynchronizedMap{ rw:new(sync.RWMutex), data:make(map[interface{}]interface{}), } }

断言 Type Assertion
  1. 仅针对 interface{} 类型。常见的如将输入传入 func foo(interface{}) , 参数自动转为 interface{} 类型

  2. 示例

    // 直接断言使用 var a interface{} val := a.(string) // 如果断言失败将panic val, ok := a.(string) // 断言失败不panic, 但ok为false // switch断言 switch val := a.(type) { default: fmt.Printf("unexpected type %T", t) // %T prints whatever type t has case bool: fmt.Printf("boolean %t\n", t) // t has type bool case *int: fmt.Printf("pointer to integer %d\n", *t) // t has type *int }

  3. 转换类型的时候如果是string可以不用断言,使用fmt.Sprint()函数可以达到想要的效果

Log日志输出
  1. 示例

    // import "log" type Logger struct { mu sync.Mutex // ensures atomic writes; protects the following fields prefix string // prefix to write at beginning of each line flag int // properties out io.Writer // destination for output buf []byte // for accumulating text to write } func New(out io.Writer, prefix string, flag int) *Logger { // 定义新的Logger return &Logger{out: out, prefix: prefix, flag: flag} } var std = New(os.Stderr, "", LstdFlags) log.SetPrefix("[UserCenter]") // 设置前缀 log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) // 设置日志开头信息 log.SetOutput(os.Stdout) log.Println(str1, str2) // output: [UserCenter]2017/04/29 05:53:26 main.go:23: <str1> <str2> // flag类型 const ( Ldate = 1 << iota //日期示例: 2009/01/23 Ltime //时间示例: 01:23:23 Lmicroseconds //毫秒示例: 01:23:23.123123. 自动替换Ltime Llongfile //绝对路径和行号: /a/b/c/d.go:23 Lshortfile //文件和行号: d.go:23. 自动替换Llongfile LUTC //日期时间转为0时区的 LstdFlags = Ldate | Ltime //Go提供的标准抬头信息 )

  2. Fatal 系列在Print系列之后调用os.Exit(1)退出; Panic 系列在调用 Print 系列函数后,调用panic函数退出并打印调用栈

    func Println(v ...interface{}) { std.Output(2, fmt.Sprintln(v...)) } func Fatalln(v ...interface{}) { std.Output(2, fmt.Sprintln(v...)) os.Exit(1) } func Panicln(v ...interface{}) { s := fmt.Sprintln(v...) std.Output(2, s) panic(s) }

  3. 分级调用示例代码 → 比较麻烦,可以考虑第三方log库,或自定义包装(根据级别调取相应的Logger)

    var ( Info *log.Logger Warning *log.Logger Error * log.Logger ) func init(){ errFile,err:=os.OpenFile("errors.log",os.O_CREATE|os.O_WRONLY|os.O_APPEND,0666) if err!=nil{ log.Fatalln("打开日志文件失败:",err) } Info = log.New(os.Stdout,"Info:",log.Ldate | log.Ltime | log.Lshortfile) Warning = log.New(os.Stdout,"Warning:",log.Ldate | log.Ltime | log.Lshortfile) Error = log.New(io.MultiWriter(os.Stderr,errFile),"Error:",log.Ldate | log.Ltime | log.Lshortfile) } func main() { Info.Println("飞雪无情的博客:","www.flysnow.org") Warning.Printf("飞雪无情的微信公众号:%s\n","flysnow_org") Error.Println("欢迎关注留言") }

  4. log获取文件信息的主要函数 runtime.Caller, 参数skip表示跳过栈帧数,0表示不跳过,也就是runtime.Caller的调用者。1的话就是再向上一层,表示调用者的调用者

    func Caller(skip int) (pc uintptr, file string, line int, ok bool)

io.Writer 和 io.Reader 接口
  1. 接口定义

    • 把数据的输入和输出,抽象为流的读写,所以只要实现了这两个接口,都可以使用流的读写功能

    // Write() 向底层数据流写入len(p)字节的数据,返回写入的字节长度n(0 <= n <= len(p)) // 如果n < len(p)或中途出错,err不为nil // 过程中不能修改切片p及其内容 type Writer interface { Write(p []byte) (n int, err error) } // Read() 从底层最多读取len(p)字节的数据到切片p,返回读取的字节数n(0 <= n <= len(p)) // 读取出错时,返回读取的字节数,且err不等于nil // 输入流结束时,返回n>0的字节,err可以为nil或EOF;但再次调用,肯定会返回0,EOF // 调用Read方法时,如果n>0时,优先处理处理读入的数据,然后再处理错误err,EOF也要这样处理 // Read方法不建议返回n=0且err=nil的情况(err等于nil不代表EOF) type Reader interface { Read(p []byte) (n int, err error) }

Context
  1. 场景

    • 等待goroutine自己结束 → sync.WaitGroup

      func main() { var wg sync.WaitGroup wg.Add(2) go func() { time.Sleep(2*time.Second) fmt.Println("1号完成") wg.Done() }() go func() { time.Sleep(2*time.Second) fmt.Println("2号完成") wg.Done() }() wg.Wait() fmt.Println("好了,大家都干完了,放工") }

    • goroutine不会自己结束,需通知goroutine结束 → chan + select

      func main() { stop := make(chan bool) go func() { for { select { case <-stop: fmt.Println("监控退出,停止了...") return default: fmt.Println("goroutine监控中...") time.Sleep(2 * time.Second) } } }() time.Sleep(10 * time.Second) fmt.Println("可以了,通知监控停止") stop<- true //为了检测监控过是否停止,如果没有监控输出,就表示停止了 time.Sleep(5 * time.Second) }

    • 多个goroutine需要控制结束 → Context

      func main() { ctx, cancel := context.WithCancel(context.Background()) go func(ctx context.Context) { for { select { case <-ctx.Done(): fmt.Println("监控退出,停止了...") return default: fmt.Println("goroutine监控中...") time.Sleep(2 * time.Second) } } }(ctx) time.Sleep(10 * time.Second) fmt.Println("可以了,通知监控停止") cancel() //为了检测监控过是否停止,如果没有监控输出,就表示停止了 time.Sleep(5 * time.Second) }

  2. 接口定义

    type Context interface { Deadline() (deadline time.Time, ok bool) // 获取截止时间 Done() <-chan struct{} // 取消channel如果可读,则说明发起了取消请求 Err() error Value(key interface{}) interface{} // 获取保存的Key-Value中的Value }

  3. 基本的两个空Context:context.Background() (主要用于main函数、初始化及测试代码)和 context.TODO() 。 二者不可取消、无deadline、无key-value

  4. context的继承衍生

    func WithCancel(parent Context) (ctx Context, cancel CancelFunc) func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) func WithValue(parent Context, key, val interface{}) Context // 不返回取消函数

  5. 使用原则

    • 显式使用context,而不是放在struct里
    • 向函数传递context时,应作为第一个参数,且不应传递nil context(可以传递context.TODO().
    • context的Value仅用于传递request-scoped data(进程、API等),而不是传递非必要数据
    • context是并发安全的,可以同时传递给多个goroutine
单元测试 基本测试
  1. 示例

    // main.go func Add(a, b int) int { return a + b } // main_test.go func TestAdd(t *testing.T) { sum := Add(1, 2) if sum == 3 { t.Log("the result is ok") } else { t.Fatal("the result is wrong") } }

  2. 基本要求

    • 含有单元测试代码的go文件必须以_test.go结尾,Go语言测试工具只认符合这个规则的文件
    • 单元测试文件名_test.go前面的部分最好是被测试的方法所在go文件的文件名,比如例子中是main_test.go,因为测试的Add函数,在main.go文件里
    • 单元测试的函数名必须以Test开头,是可导出公开的函数
    • 测试函数的签名必须接收一个指向testing.T类型的指针,并且不能返回任何值
    • 函数名最好是Test+要测试的方法函数名,比如例子中是TestAdd,表示测试的是Add这个这个函数
表组测试

对多组样例进行测试

func TestAdd(t *testing.T) { sum := Add(1,2) if sum == 3 { t.Log("the result is ok") } else { t.Fatal("the result is wrong") } sum = Add(3,4) if sum == 7 { t.Log("the result is ok") } else { t.Fatal("the result is wrong") } } 模拟调用 - 网络

  1. 单元测试的原则,就是所测方法不要受到所依赖环境的影响。所以对于联网等场景,需要进行模拟调用

  2. 标准库中提供了127.0.0.1:10000/debug/pprof/就能看到监控的一些信息了 // 可视化界面(需安装graphviz)的两种方法 go tool pprof localhost:10000/debug/pprof/profile go tool pprof -localhost:10000/debug/pprof/profile

文件读写
  1. os

    // import "os" /* 文件(夹)增删 */ func Mkdir(name string, perm FileMode) error // mkdir func MkdirAll(name string, perm FileMode) error // mkdir -p func Remove(name string) error // rm -f func RemoveAll(path string) error // rm -rf /* 文件处理 */ func Create(name string) (file *File, err error) // 创建文件,默认权限0666 func Open(name string) (file *File, err error) // 打开文件(只读) func OpenFile(name string, flag int, perm uint32) // 以flag方式打开文件,perm为权限 // flag包含:[O_RDONLY 或 O_WRONLY 或 O_RDWR] | {O_APPEND, O_CREATE[|O_EXCL], O_SYNC,O_TRUNC} func (file *File) Write(b []byte) (n int, err error) func (file *File) WriteAt(b []byte, off int64) (n int, err error) func (file *File) WriteString(s string) (ret int, err error) func (file *File) Read(b []byte) (n int, err error) func (file *File) ReadAt(b []byte, off int64) (n int, err error)

  2. io 和 ioutil

    // io包为I/O原语定义了基本的接口 type Reader interface { Read(p []byte) (n int, err error) } type ReaderAt interface { ReadAt(p []byte, off int64) (n int, err error) } type Writer interface { Write(p []byte) (n int, err error) } type WriterAt interface { WriteAt(p []byte, off int64) (n int, err error) }

    // import "io/ioutil" func ReadFile(filename string) ([]byte, error) // 读取文件所有数据 func WriteFile(filename string, data []byte, perm os.FileMode) error // 创建或清空文件并写入数据 func ReadDir(dirname string) ([]os.FileInfo, error) // 读取目录中的所有目录及文件(不包含子目录),列表是经过排序的 func TempFile(dir, prefix) (f *os.File, err error) // 在dir目录创建以prefix开头的临时文件,多次调用会创建不同的临时文件(若dir为空,则在默认临时目录 os.TempDir()。临时文件需要自己删除 func TempDir(dir, prefix string) (name string, err error) // 同上类似 func ReadAll(r io.Reader) ([]byte, error) // 读取所有数据 func NopCloser(r io.Reader) io.ReadCloser // 包装Reader为ReadCloser类型,增加一个no-op方法

  3. 文件复制示例

    可以使用 io.Copy()或者使用 ioutil.WriteFile()+ioutil.ReadFile()进行文件复制,但最高效的还是使用边读边写的方式

    //打开源文件 fileRead,err :=os.Open("/tmp/test.txt") if err != nil { fmt.Println("Open err:",err) return } defer fileRead.Close() //创建目标文件 fileWrite,err := os.Create("/tmp/test_copy.txt") if err != nil { fmt.Println("Create err:",err) return } defer fileWrite.Close() //从源文件获取数据,放到缓冲区 buf :=make([]byte, 4096) //循环从源文件中获取数据,全部写到目标文件中 for { n,err := fileRead.Read(buf) if err != nil && err == io.EOF { fmt.Printf("读取完毕,n = d%\n:",n) return } fileWrite.Write(buf[:n]) //读多少、写多少 }

本文共计8080个文字,预计阅读时间需要33分钟。

Go语言笔记如何高效学习?

变量声明+go语言中局部变量声明后必须使用,否则报错。全局变量无此限制+整数、浮点数、string等基本类型为值类型,赋值后变量名指向各自独立的值;而复杂类型为引用类型,两个变量引用同一内存地址。

变量声明
  • go语言中局部变量声明后必须被使用,否则报错。全局变量无此限制
  • 整数、浮点数、string这些基本类型为值类型,赋值后变量名指向各自的值;而复杂类型为引用类型,两变量赋值后指向同一内存区域
  • 变量声明时若不赋值则为系统默认值(数值类型0, 布尔false, string为"",其他为nil)

// 一般变量声明的3种方法 // 第一种: var 变量名 类型 var i int // 第二种(省略类型,由初值自动判断): var 变量名 var i = 5 //第三种(省略类型和var关键字,但只能用于函数体内),如: str := "string" // := 声明符前面必须有一个变量之前未被声明,而不能单纯用来给已声明变量赋值,否则报错 // 多变量声明 var i, j = 5, 7 // 三种方法均可 i, j = j, i // swap values var ( // 这种因式分解关键字的写法一般用于声明全局变量 a int b bool ) // 空白标识符_(实际上无法访问值,用于丢弃函数的某个返回值) _, d = foo()

常量声明:

// 格式:const identifier [type] = value const f, s = 5.5, "hello" //const可用于枚举,缺省时将使用上一行的表达式 // iota为系统特殊变量,每次遇到const时置0, 每定义一个变量值加1 const( a = iota //0 b //1 c //2 d = "ha" //独立值,iota += 1 e //"ha" iota += 1 f = 100 //iota +=1 g //100 iota +=1 h = iota //7,恢复计数 i //8 ) 语言结构

  1. 一行只有一条语句时可省略分号

// 当前程序的包名 package main // 导入其他包 import . "fmt" // 导入所有函数和变量(不建议) import _ "fmt" // 仅导入,运行init函数,不导入内部变量和函数 // 常量定义 const identifier [type] = value const PI = 3.14 // 全局变量的声明和赋值 var name = "gopher" // 一般类型声明 type newType int // 结构的声明 type gopher struct{} // 接口的声明 type golang interface{} // 由main函数作为程序入口点启动 func main() { Println("Hello World!") } 运算符

  1. 算术:+ - * / % ++(自增) --(自减)
  2. 关系:> >= < <= == !=
  3. 逻辑:&& || !
  4. 位:& | ^ << >>^ 做一元运算符为位取反,二元为异或)
  5. 赋值运算符 := 以及算术、位运算符结合=的赋值运算符
  6. 其他:&(取地址运算符)、* (取对应地址值)
优先级 运算符 5 * / % << >> & &^ 4 + - | ^ 3 == != < <= > >= 2 && 1 || 数组
  1. 声明和初始化

    var array [5]int // 未初始化默认零值 array := [...]int{1, 2, 3} // “...” 自动确定大小 array := [5]int{1: 1, 3: 5} // 初始化arr[1] = 1, arr[3] = 5 array := [5]*int{1: new(int), 3: new(int)}

  2. 数组采用值传递,需满足大小和类型相同才能赋值。若需要在函数中修改,用数组指针传参

    func main() { array := [5]int{1, 2, 3} modify(&array) } func modify(arr *[5]int) { // 注意区别于指针数组类型 [5]*int arr[1] = 5 }

切片
  1. 定义&初始化

    // 定义 slice := make([]int, 5) // len = cap = 5 slice := make([]int, 5, 10) // len = 5, cap = 10 slice := []int{1:3, 3:5} // 基于现有数组或切片创建,指向原有(底层)数组,相当于引用赋值。取值范围[i, j]代表区间[i,j),此时长度为 len-i, 容量为 cap-i。i,j可省略,此时分别代表0,len // 还可加第三个参数,即[i:j:k], k代表切片容量容量右区间(k<=原cap) slice1 := slice[:] slice2 : slice[:5] // 切片清空 (比如清空缓存) buf = buf[:0] var slice []int // nil切片,指向底层数组的指针为nil slice := []int{} // 空切片,指向底层数组的指针为一个地址

  2. 底层采用数组存储,包含3个字段:

    • 指向底层数组的指针
    • 切片长度
    • 切片容量(仅用于扩展容量,超出长度的部分不能用于索引)
  3. 可通过内置函数 len()cap() 获取切片长度和容量

  4. 赋值为引用传递,此时对切片元素的修改会影响原切片。可用内置的append()对赋值的变量追加元素,若此时底层数组容量足够,则直接对元素进行覆盖(但原切片len和cap不变)。否则新建一个底层数组.

  5. 数组和切片迭代

    for i := 0; i < len(slice); i++ {} for idx, val := range slice {} // idx 可改成用下划线“_”省略

Map映射
  1. 定义&初始化

    // make dict := make(map[string]int) // 字面量初始化 dict := map[string]int{"张三": 38, "李四": 20} dict := map[string]int{} // 空map var dict map[string]int // nil映射,无空间,使用前需用make赋值 dict = make(map[string]int) dict["张三"] = 38

  2. 取值 & 删除

    age := dict["张三"] // 若键不存在,返回零值 age, exists := dict["张三"] // exists 布尔类型,表示值是否存在 delete(dict, "张三") // 若map中存在对应key,删除之

  3. 赋值采用引用传递

  4. Map存储的是无序键值对的组合,遍历结果不确定。需要有序结果的话需要先排序

    // 遍历方式 for key := range dict {} for key, val := range dict {} var names []string for name := range dict { names = append(names, name) } sort.Strings(names) // 排序 for _, key := range names { fmt.Println(key, dict[key]) }

  5. map的键类型可以是任意值类型,内置 or 结构类型,必须能用 == 比较。而map、slice、函数、及含有切片的结构类型等引用类型则不能作为键

结构类型
  1. 基础类型:包括整型、浮点型、字符型、布尔型, 赋值采用值传递。对它们的操作一般返回一个新创建的值,所以是线程安全的。

  2. 引用类型:包括map、slice、chan、函数类型、接口类型,采用引用传递,本质是传递了底层的指针值(注意slice结构中还包含了len和cap两个基本类型,不会在函数中被修改)

  3. 结构类型

    • 采用值传递,但结构体内的引用类型采用引用传递(本质是指针的值传递)
    • 若要修改结构体值,应传递指针

    type person struct{ name string age int } var p person // 声明并初始化变量,默认零值 p := person{"Jack", 5} // 初始化顺序和声明的相同 p := person{age: 5, name: "Jack"} // 不按顺序 // 在函数中修改值 func main() { jim := person{"Jim",10} fmt.Println(jim) modify(&jim) fmt.Println(jim) } func modify(p *person) { p.age =p.age+10 // golang中一律用句号“.”访问成员 }

  4. 自定义类型:定义结构体 或 基于已有类型

    • Go是强类型语言。即使底层类型相同,如果类型名不同也不能相互赋值

      type Duration int64 var dur Duration dur = int64(100) // Error fmt.Println(dur)

    • 基于已有类型重新定义类型的好处

      • 添加方法
      • 明确业务含义
函数&方法
  1. 函数

    • 函数定义中没有接收者
  2. 方法

    • 方法定义中有接收者

      type person struct{ name string age int } func (p person) String() {} // 值类型接收者 func (p *person) modify() {} // 指针型接收者 // 调用时不必严格采用对应的值或指针的类型,编译器或自动进行类型转换 // 比如var mu sync.Mutex调用mu.Lock()函数 var p person p.modify() (&p).modify() // 两种均可

  3. 大小写

    • 函数、方法、变量名、类型的首字母若大写,则可在包外使用,相当于java的public关键字
  4. 多值返回

    func add(a, b int) (int, error) { return a + b, nil // 返回顺序与声明顺序一致 } func main() { sum, err := add(1, 2) if err != nil { log.Fatal(err) return } fmt.Println(sum) }

  5. 可变参数

    • 函数入参表类型前加 ... ,必须是最后一个入参。 函数内该参数相当于一个数组

    func main() { print("1","2","3") } func print (a ...interface{}){ for _,v:=range a{ fmt.Print(v) } fmt.Println() }

接口
  1. 接口是抽象的,仅包含一组接口方法。具体实现由用户实现

  2. 如果用户定义的类型(定义为 实体类型),实现了接口类型声明的所有方法,那么这个用户定义的类型就实现了这个接口

  3. 多态:实体类型对象可以赋值给对应的接口类型对象。那么在调用接口类型对象的方法时,将转化成对具体实体类型方法的调用

  4. 接口类型被赋值后,包含两个指针。第一个指针指向存储的实体类型的信息和相关联的方法集,第二个指针指向存储的实体类型的值

  5. 方法集

    • 值接收 的 方法,实体类型的值和指针都可以实现对应的接口(指针传递不会被改变值);而若采用 指针接收, 只有实体类型的指针能够实现对应的接口

      func main() { var c cat //值作为参数传递 invoke(&c) // 虽然以指针传递,但不会在方法中改变值 } //需要一个animal接口作为参数 func invoke(a animal){ a.printInfo() } type animal interface { printInfo() } type cat int //值接收者实现animal接口 func (c cat) printInfo(){ fmt.Println("a cat") }

    • Methods Receivers Values (t T) T and *T (t *T) *T
嵌入类型
  1. 示例

    func main() { ad := admin{user{"张三","zhangsan@flysnow.org"},"管理员"} // 必须按定义的方式初始化 ad.user.sayHello() // 被覆盖的方法依然存在 ad.sayHello() invoke(ad) fmt.Println(ad.name) // user成员同时变成admin成员 } type user struct { name string email string } type admin struct { user // 将已声明的结构体嵌入 level string } func (u user) sayHello(){ fmt.Println("Hello,i am a user") } func (a admin) sayHello(){ // 方法重写override fmt.Println("Hello,i am a admin") } type Hello interface { hi() } func (u user) hi() { // user实现了Hello接口,则admin也实现了该接口 fmt.Println("Hi. I'm a user.") } func invoke(person Hello) { person.hi() }

  2. Go语言中没有继承的概念。Go提倡的代码组合方式是组合。嵌入的为内部类型,包含的为外部类型

  3. 性质

    • 嵌入后,内部类型的成员便也成为外部类型的成员
    • 在外部类型中可以对内部类型的方法进行重写。但内部类型中的方法依然存在,可以通过内部类型去调用
    • 如果内部实现了某个接口,则外部类型也实现了该接口
标识符可见性
  1. 示例

    package common import "fmt" func NewLoginer() Loginer{ // 内部设计改变时,不影响用户调用的接口 return defaultLogin(0) } type Loginer interface { Login() } type defaultLogin int func (d defaultLogin) Login(){ fmt.Println("login in...") }

    // -------------- main包 -----------// package main func main() { l:=common.NewLoginer() l.Login() }

  2. 范围:变量、函数、类型、成员(变量/函数)

  3. 通过首字母大小写定义可见性,大写exported,小写unexported

  4. .操作符前面的部分导出了,.操作符后面的部分才有可能被访问;如果.前面的部分都没有导出,那么即使.后面的部分是导出的,也无法访问

    例子 可否访问 Admin.User.Name 是 Admin.User.name 否 Admin.user.Name 否 Admin.user.name 否
Goroutine
  1. 基本概念

    概念 说明 进程 一个程序对应的一个独立程序空间 线程 一个执行空间,一个进程可以有多个线程 逻辑处理器 执行创建的goroutine,绑定一个线程 调度器 Go运行时中的,分配goroutine给不同的逻辑处理器 全局运行队列 所有刚创建的goroutine都会放到这里 本地运行队列 逻辑处理器的goroutine队列
  2. go语言中并发指的是让某个函数独立于其他函数运行的能力,一个goroutine就是一个独立的工作单元,Go的runtime(运行时)会在逻辑处理器上调度这些goroutine来运行,一个逻辑处理器绑定一个操作系统线程,所以说goroutine不是线程,它是一个协程,也是这个原因,它是由Go语言运行时本身的算法实现的

  3. 当我们创建一个goroutine的后,会先存放在全局运行队列中,等待Go运行时的调度器进行调度,把他们分配给其中的一个逻辑处理器,并放到这个逻辑处理器对应的本地运行队列中,最终等着被逻辑处理器执行即可

  4. 设置逻辑处理器个数等于物理核数

    runtime.GOMAXPROCS(runtime.NumCPU())

资源竞争 (data race)
  1. 当两个或多个goroutine在没有相互同步的情况下访问某个共享的资源时,就会出现资源竞争(Data Race)

    var ( count int32 wg sync.WaitGroup ) func main() { wg.Add(2) go incCount() go incCount() wg.Wait() fmt.Println(count) // 结果不确定 } func incCount() { defer wg.Done() for i := 0; i < 2; i++ { value := count runtime.Gosched() value++ count = value } }

  2. 检查data race的方式

    go run -race main.go go build -race // then run the binary generated

  3. 避免并发的方式

    // import "sync.atomic" atomic.AddInt32(&count, 1) // import "sync" var mu sync.Mutex // 互斥锁 mu.Lock() mu.Unlock() // 使用channel

通道Channel
  1. 定义 & 基本使用

    // 双向通道 ch := make(chan int) // 无缓冲通道(同步通道) ch := make(chan int, 5) // 缓冲通道,缓冲容量为5 var ch chan int // nil通道 // 单向通道,仅向通道发送 chan<- int, 仅从通道接收 <-chan int。虽然也能用make新建,但是一个不能填充数据(发送)只能读取的通道是毫无意义的 var ch2 <-chan int var ch2 chan<- int ch2 = ch // 将双向通道赋值给单向通道 ch <- 5 <- ch2 // 获取通道容量和通道内的元素个数 n := len(ch) m := cap(ch) // 关闭通道,此时往channel发送会panic,从channel接收返回零值 close(ch) val, ok := <-ch // ok 表示channel是否已关闭

  2. 无缓冲通道要求发送和接收端都需要做好准备,否则一方将阻塞,等待另一方处理,所以又称同步通道。缓冲通道发送后并不要求立马接收

  3. 只有双向通道才能关闭,且只能关闭一次。重复关闭channel或往关闭了的channel发送数据会panic

并发示例
  1. 并发示例Runner

    package common import ( "errors" "os" "os/signal" "time" ) var ErrTimeOut = errors.New("执行者执行超时") var ErrInterrupt = errors.New("执行者被中断") //一个执行者,可以执行任何任务,但是这些任务是限制完成的, //该执行者可以通过发送终止信号终止它 type Runner struct { tasks []func(int) //要执行的任务 complete chan error //用于通知任务全部完成 timeout <-chan time.Time //这些任务在多久内完成 interrupt chan os.Signal //可以控制强制终止的信号 } func New(tm time.Duration) *Runner { return &Runner{ complete: make(chan error), timeout: time.After(tm), interrupt: make(chan os.Signal, 1), } } //将需要执行的任务,添加到Runner里 func (r *Runner) Add(tasks ...func(int)) { r.tasks = append(r.tasks, tasks...) } //执行任务,执行的过程中接收到中断信号时,返回中断错误 //如果任务全部执行完,还没有接收到中断信号,则返回nil func (r *Runner) run() error { for id, task := range r.tasks { if r.isInterrupt() { return ErrInterrupt } task(id) } return nil } //检查是否接收到了中断信号 func (r *Runner) isInterrupt() bool { select { case <-r.interrupt: signal.Stop(r.interrupt) return true default: return false } } //开始执行所有任务,并且监视通道事件 func (r *Runner) Start() error { //希望接收哪些系统信号 signal.Notify(r.interrupt, os.Interrupt) go func() { r.complete <- r.run() }() select { case err := <-r.complete: return err case <-r.timeout: return ErrTimeOut } }

    package main import ( "flysnow.org/hello/common" "log" "time" "os" ) func main() { log.Println("...开始执行任务...") timeout := 3 * time.Second r := common.New(timeout) r.Add(createTask(), createTask(), createTask()) if err := r.Start(); err != nil{ switch err { case common.ErrTimeOut: log.Println(err) os.Exit(1) case common.ErrInterrupt: log.Println(err) os.Exit(2) } } log.Println("...任务执行结束...") } func createTask() func(int) { return func(id int) { log.Printf("正在执行任务%d", id) time.Sleep(time.Duration(id)* time.Second) } }

  2. 资源共享池 -> Pool

    package common import ( "errors" "io" "sync" "log" ) //一个安全的资源池,被管理的资源必须都实现io.Close接口 type Pool struct { m sync.Mutex res chan io.Closer factory func() (io.Closer, error) closed bool } var ErrPoolClosed = errors.New("资源池已经被关闭。") //创建一个资源池 func New(fn func() (io.Closer, error), size uint) (*Pool, error) { if size <= 0 { return nil, errors.New("size的值太小了。") } return &Pool{ factory: fn, res: make(chan io.Closer, size) }, nil } //从资源池里获取一个资源 func (p *Pool) Acquire() (io.Closer,error) { select { case r,ok := <-p.res: log.Println("Acquire:共享资源") if !ok { return nil,ErrPoolClosed } return r,nil default: log.Println("Acquire:新生成资源") return p.factory() } } //关闭资源池,释放资源 func (p *Pool) Close() { p.m.Lock() defer p.m.Unlock() if p.closed { return } p.closed = true //关闭通道,不让写入了 close(p.res) //关闭通道里的资源 for r:=range p.res { r.Close() } } func (p *Pool) Release(r io.Closer){ //保证该操作和Close方法的操作是安全的 p.m.Lock() defer p.m.Unlock() //资源池都关闭了,就剩这一个没有释放的资源了,释放即可 if p.closed { r.Close() return } select { case p.res <- r: log.Println("资源释放到池子里了") default: log.Println("资源池满了,释放这个资源吧") r.Close() } }

    package main import ( "flysnow.org/hello/common" "io" "log" "math/rand" "sync" "sync/atomic" "time" ) const ( //模拟的最大goroutine maxGoroutine = 5 //资源池的大小 poolRes = 2 ) func main() { //等待任务完成 var wg sync.WaitGroup wg.Add(maxGoroutine) p, err := common.New(createConnection, poolRes) if err != nil { log.Println(err) return } //模拟好几个goroutine同时使用资源池查询数据 for query := 0; query < maxGoroutine; query++ { go func(q int) { dbQuery(q, p) wg.Done() }(query) } wg.Wait() log.Println("开始关闭资源池") p.Close() } //模拟数据库查询 func dbQuery(query int, pool *common.Pool) { conn, err := pool.Acquire() if err != nil { log.Println(err) return } defer pool.Release(conn) //模拟查询 time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond) log.Printf("第%d个查询,使用的是ID为%d的数据库连接", query, conn.(*dbConnection).ID) } //数据库连接 type dbConnection struct { ID int32//连接的标志 } //实现io.Closer接口 func (db *dbConnection) Close() error { log.Println("关闭连接", db.ID) return nil } var idCounter int32 //生成数据库连接的方法,以供资源池使用 func createConnection() (io.Closer, error) { //并发安全,给数据库连接生成唯一标志 id := atomic.AddInt32(&idCounter, 1) return &dbConnection{id}, nil }

  3. 补充:原生资源池 sync.Pool

    • 资源池大小默认无上限
    • 缓存的对象是临时的,在下一次GC时将会被清除
    • Get() 从资源池获取资源,Put() 放入资源池,返回任意对象 interface{}

    package main import ( "log" "math/rand" "sync" "sync/atomic" "time" ) const ( //模拟的最大goroutine maxGoroutine = 5 ) func main() { //等待任务完成 var wg sync.WaitGroup wg.Add(maxGoroutine) p:=&sync.Pool{ // 通过字面量声明 New: createConnection, } //模拟好几个goroutine同时使用资源池查询数据 for query := 0; query < maxGoroutine; query++ { go func(q int) { dbQuery(q, p) wg.Done() }(query) } wg.Wait() } //模拟数据库查询 func dbQuery(query int, pool *sync.Pool) { conn:=pool.Get().(*dbConnection) defer pool.Put(conn) //模拟查询 time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond) log.Printf("第%d个查询,使用的是ID为%d的数据库连接", query, conn.ID) } //数据库连接 type dbConnection struct { ID int32//连接的标志 } //实现io.Closer接口 func (db *dbConnection) Close() error { log.Println("关闭连接", db.ID) return nil } var idCounter int32 //生成数据库连接的方法,以供资源池使用 func createConnection() interface{} { //并发安全,给数据库连接生成唯一标志 id := atomic.AddInt32(&idCounter, 1) return &dbConnection{ID:id} }

读写锁
  1. 原因:读 - 读不互斥,读 - 写、写 - 写操作才互斥。直接采用sync.Mutex效率较低

    Go语言笔记如何高效学习?

  2. 示例

    var mu sync.RWMutex // 读锁 mu.RLock() mu.RUnlock() // 写锁 mu.Lock() mu.Unlock()

  3. 示例 SynchronizedMap

    package common import ( "sync" ) //安全的Map type SynchronizedMap struct { rw *sync.RWMutex data map[interface{}]interface{} } //存储操作 func (sm *SynchronizedMap) Put(k,v interface{}){ sm.rw.Lock() defer sm.rw.Unlock() sm.data[k]=v } //获取操作 func (sm *SynchronizedMap) Get(k interface{}) interface{}{ sm.rw.RLock() defer sm.rw.RUnlock() return sm.data[k] } //删除操作 func (sm *SynchronizedMap) Delete(k interface{}) { sm.rw.Lock() defer sm.rw.Unlock() delete(sm.data,k) } //遍历Map,并且把遍历的值给回调函数,可以让调用者控制做任何事情 func (sm *SynchronizedMap) Each(cb func (interface{},interface{})){ sm.rw.RLock() defer sm.rw.RUnlock() for k, v := range sm.data { cb(k,v) } } //生成初始化一个SynchronizedMap func NewSynchronizedMap() *SynchronizedMap{ return &SynchronizedMap{ rw:new(sync.RWMutex), data:make(map[interface{}]interface{}), } }

断言 Type Assertion
  1. 仅针对 interface{} 类型。常见的如将输入传入 func foo(interface{}) , 参数自动转为 interface{} 类型

  2. 示例

    // 直接断言使用 var a interface{} val := a.(string) // 如果断言失败将panic val, ok := a.(string) // 断言失败不panic, 但ok为false // switch断言 switch val := a.(type) { default: fmt.Printf("unexpected type %T", t) // %T prints whatever type t has case bool: fmt.Printf("boolean %t\n", t) // t has type bool case *int: fmt.Printf("pointer to integer %d\n", *t) // t has type *int }

  3. 转换类型的时候如果是string可以不用断言,使用fmt.Sprint()函数可以达到想要的效果

Log日志输出
  1. 示例

    // import "log" type Logger struct { mu sync.Mutex // ensures atomic writes; protects the following fields prefix string // prefix to write at beginning of each line flag int // properties out io.Writer // destination for output buf []byte // for accumulating text to write } func New(out io.Writer, prefix string, flag int) *Logger { // 定义新的Logger return &Logger{out: out, prefix: prefix, flag: flag} } var std = New(os.Stderr, "", LstdFlags) log.SetPrefix("[UserCenter]") // 设置前缀 log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) // 设置日志开头信息 log.SetOutput(os.Stdout) log.Println(str1, str2) // output: [UserCenter]2017/04/29 05:53:26 main.go:23: <str1> <str2> // flag类型 const ( Ldate = 1 << iota //日期示例: 2009/01/23 Ltime //时间示例: 01:23:23 Lmicroseconds //毫秒示例: 01:23:23.123123. 自动替换Ltime Llongfile //绝对路径和行号: /a/b/c/d.go:23 Lshortfile //文件和行号: d.go:23. 自动替换Llongfile LUTC //日期时间转为0时区的 LstdFlags = Ldate | Ltime //Go提供的标准抬头信息 )

  2. Fatal 系列在Print系列之后调用os.Exit(1)退出; Panic 系列在调用 Print 系列函数后,调用panic函数退出并打印调用栈

    func Println(v ...interface{}) { std.Output(2, fmt.Sprintln(v...)) } func Fatalln(v ...interface{}) { std.Output(2, fmt.Sprintln(v...)) os.Exit(1) } func Panicln(v ...interface{}) { s := fmt.Sprintln(v...) std.Output(2, s) panic(s) }

  3. 分级调用示例代码 → 比较麻烦,可以考虑第三方log库,或自定义包装(根据级别调取相应的Logger)

    var ( Info *log.Logger Warning *log.Logger Error * log.Logger ) func init(){ errFile,err:=os.OpenFile("errors.log",os.O_CREATE|os.O_WRONLY|os.O_APPEND,0666) if err!=nil{ log.Fatalln("打开日志文件失败:",err) } Info = log.New(os.Stdout,"Info:",log.Ldate | log.Ltime | log.Lshortfile) Warning = log.New(os.Stdout,"Warning:",log.Ldate | log.Ltime | log.Lshortfile) Error = log.New(io.MultiWriter(os.Stderr,errFile),"Error:",log.Ldate | log.Ltime | log.Lshortfile) } func main() { Info.Println("飞雪无情的博客:","www.flysnow.org") Warning.Printf("飞雪无情的微信公众号:%s\n","flysnow_org") Error.Println("欢迎关注留言") }

  4. log获取文件信息的主要函数 runtime.Caller, 参数skip表示跳过栈帧数,0表示不跳过,也就是runtime.Caller的调用者。1的话就是再向上一层,表示调用者的调用者

    func Caller(skip int) (pc uintptr, file string, line int, ok bool)

io.Writer 和 io.Reader 接口
  1. 接口定义

    • 把数据的输入和输出,抽象为流的读写,所以只要实现了这两个接口,都可以使用流的读写功能

    // Write() 向底层数据流写入len(p)字节的数据,返回写入的字节长度n(0 <= n <= len(p)) // 如果n < len(p)或中途出错,err不为nil // 过程中不能修改切片p及其内容 type Writer interface { Write(p []byte) (n int, err error) } // Read() 从底层最多读取len(p)字节的数据到切片p,返回读取的字节数n(0 <= n <= len(p)) // 读取出错时,返回读取的字节数,且err不等于nil // 输入流结束时,返回n>0的字节,err可以为nil或EOF;但再次调用,肯定会返回0,EOF // 调用Read方法时,如果n>0时,优先处理处理读入的数据,然后再处理错误err,EOF也要这样处理 // Read方法不建议返回n=0且err=nil的情况(err等于nil不代表EOF) type Reader interface { Read(p []byte) (n int, err error) }

Context
  1. 场景

    • 等待goroutine自己结束 → sync.WaitGroup

      func main() { var wg sync.WaitGroup wg.Add(2) go func() { time.Sleep(2*time.Second) fmt.Println("1号完成") wg.Done() }() go func() { time.Sleep(2*time.Second) fmt.Println("2号完成") wg.Done() }() wg.Wait() fmt.Println("好了,大家都干完了,放工") }

    • goroutine不会自己结束,需通知goroutine结束 → chan + select

      func main() { stop := make(chan bool) go func() { for { select { case <-stop: fmt.Println("监控退出,停止了...") return default: fmt.Println("goroutine监控中...") time.Sleep(2 * time.Second) } } }() time.Sleep(10 * time.Second) fmt.Println("可以了,通知监控停止") stop<- true //为了检测监控过是否停止,如果没有监控输出,就表示停止了 time.Sleep(5 * time.Second) }

    • 多个goroutine需要控制结束 → Context

      func main() { ctx, cancel := context.WithCancel(context.Background()) go func(ctx context.Context) { for { select { case <-ctx.Done(): fmt.Println("监控退出,停止了...") return default: fmt.Println("goroutine监控中...") time.Sleep(2 * time.Second) } } }(ctx) time.Sleep(10 * time.Second) fmt.Println("可以了,通知监控停止") cancel() //为了检测监控过是否停止,如果没有监控输出,就表示停止了 time.Sleep(5 * time.Second) }

  2. 接口定义

    type Context interface { Deadline() (deadline time.Time, ok bool) // 获取截止时间 Done() <-chan struct{} // 取消channel如果可读,则说明发起了取消请求 Err() error Value(key interface{}) interface{} // 获取保存的Key-Value中的Value }

  3. 基本的两个空Context:context.Background() (主要用于main函数、初始化及测试代码)和 context.TODO() 。 二者不可取消、无deadline、无key-value

  4. context的继承衍生

    func WithCancel(parent Context) (ctx Context, cancel CancelFunc) func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) func WithValue(parent Context, key, val interface{}) Context // 不返回取消函数

  5. 使用原则

    • 显式使用context,而不是放在struct里
    • 向函数传递context时,应作为第一个参数,且不应传递nil context(可以传递context.TODO().
    • context的Value仅用于传递request-scoped data(进程、API等),而不是传递非必要数据
    • context是并发安全的,可以同时传递给多个goroutine
单元测试 基本测试
  1. 示例

    // main.go func Add(a, b int) int { return a + b } // main_test.go func TestAdd(t *testing.T) { sum := Add(1, 2) if sum == 3 { t.Log("the result is ok") } else { t.Fatal("the result is wrong") } }

  2. 基本要求

    • 含有单元测试代码的go文件必须以_test.go结尾,Go语言测试工具只认符合这个规则的文件
    • 单元测试文件名_test.go前面的部分最好是被测试的方法所在go文件的文件名,比如例子中是main_test.go,因为测试的Add函数,在main.go文件里
    • 单元测试的函数名必须以Test开头,是可导出公开的函数
    • 测试函数的签名必须接收一个指向testing.T类型的指针,并且不能返回任何值
    • 函数名最好是Test+要测试的方法函数名,比如例子中是TestAdd,表示测试的是Add这个这个函数
表组测试

对多组样例进行测试

func TestAdd(t *testing.T) { sum := Add(1,2) if sum == 3 { t.Log("the result is ok") } else { t.Fatal("the result is wrong") } sum = Add(3,4) if sum == 7 { t.Log("the result is ok") } else { t.Fatal("the result is wrong") } } 模拟调用 - 网络

  1. 单元测试的原则,就是所测方法不要受到所依赖环境的影响。所以对于联网等场景,需要进行模拟调用

  2. 标准库中提供了127.0.0.1:10000/debug/pprof/就能看到监控的一些信息了 // 可视化界面(需安装graphviz)的两种方法 go tool pprof localhost:10000/debug/pprof/profile go tool pprof -localhost:10000/debug/pprof/profile

文件读写
  1. os

    // import "os" /* 文件(夹)增删 */ func Mkdir(name string, perm FileMode) error // mkdir func MkdirAll(name string, perm FileMode) error // mkdir -p func Remove(name string) error // rm -f func RemoveAll(path string) error // rm -rf /* 文件处理 */ func Create(name string) (file *File, err error) // 创建文件,默认权限0666 func Open(name string) (file *File, err error) // 打开文件(只读) func OpenFile(name string, flag int, perm uint32) // 以flag方式打开文件,perm为权限 // flag包含:[O_RDONLY 或 O_WRONLY 或 O_RDWR] | {O_APPEND, O_CREATE[|O_EXCL], O_SYNC,O_TRUNC} func (file *File) Write(b []byte) (n int, err error) func (file *File) WriteAt(b []byte, off int64) (n int, err error) func (file *File) WriteString(s string) (ret int, err error) func (file *File) Read(b []byte) (n int, err error) func (file *File) ReadAt(b []byte, off int64) (n int, err error)

  2. io 和 ioutil

    // io包为I/O原语定义了基本的接口 type Reader interface { Read(p []byte) (n int, err error) } type ReaderAt interface { ReadAt(p []byte, off int64) (n int, err error) } type Writer interface { Write(p []byte) (n int, err error) } type WriterAt interface { WriteAt(p []byte, off int64) (n int, err error) }

    // import "io/ioutil" func ReadFile(filename string) ([]byte, error) // 读取文件所有数据 func WriteFile(filename string, data []byte, perm os.FileMode) error // 创建或清空文件并写入数据 func ReadDir(dirname string) ([]os.FileInfo, error) // 读取目录中的所有目录及文件(不包含子目录),列表是经过排序的 func TempFile(dir, prefix) (f *os.File, err error) // 在dir目录创建以prefix开头的临时文件,多次调用会创建不同的临时文件(若dir为空,则在默认临时目录 os.TempDir()。临时文件需要自己删除 func TempDir(dir, prefix string) (name string, err error) // 同上类似 func ReadAll(r io.Reader) ([]byte, error) // 读取所有数据 func NopCloser(r io.Reader) io.ReadCloser // 包装Reader为ReadCloser类型,增加一个no-op方法

  3. 文件复制示例

    可以使用 io.Copy()或者使用 ioutil.WriteFile()+ioutil.ReadFile()进行文件复制,但最高效的还是使用边读边写的方式

    //打开源文件 fileRead,err :=os.Open("/tmp/test.txt") if err != nil { fmt.Println("Open err:",err) return } defer fileRead.Close() //创建目标文件 fileWrite,err := os.Create("/tmp/test_copy.txt") if err != nil { fmt.Println("Create err:",err) return } defer fileWrite.Close() //从源文件获取数据,放到缓冲区 buf :=make([]byte, 4096) //循环从源文件中获取数据,全部写到目标文件中 for { n,err := fileRead.Read(buf) if err != nil && err == io.EOF { fmt.Printf("读取完毕,n = d%\n:",n) return } fileWrite.Write(buf[:n]) //读多少、写多少 }