Go语言笔记如何高效学习?
- 内容介绍
- 文章标签
- 相关推荐
本文共计8080个文字,预计阅读时间需要33分钟。
变量声明+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
)
语言结构
- 一行只有一条语句时可省略分号
// 当前程序的包名
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!")
}
运算符
- 算术:
+ - * / % ++(自增) --(自减) - 关系:
> >= < <= == != - 逻辑:
&& || ! - 位:
& | ^ << >>(^做一元运算符为位取反,二元为异或) - 赋值运算符 := 以及算术、位运算符结合=的赋值运算符
- 其他:&(取地址运算符)、* (取对应地址值)
-
声明和初始化
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)} -
数组采用值传递,需满足大小和类型相同才能赋值。若需要在函数中修改,用数组指针传参
func main() { array := [5]int{1, 2, 3} modify(&array) } func modify(arr *[5]int) { // 注意区别于指针数组类型 [5]*int arr[1] = 5 }
-
定义&初始化
// 定义 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{} // 空切片,指向底层数组的指针为一个地址 -
底层采用数组存储,包含3个字段:
- 指向底层数组的指针
- 切片长度
- 切片容量(仅用于扩展容量,超出长度的部分不能用于索引)
-
可通过内置函数
len()和cap()获取切片长度和容量 -
赋值为引用传递,此时对切片元素的修改会影响原切片。可用内置的
append()对赋值的变量追加元素,若此时底层数组容量足够,则直接对元素进行覆盖(但原切片len和cap不变)。否则新建一个底层数组. -
数组和切片迭代
for i := 0; i < len(slice); i++ {} for idx, val := range slice {} // idx 可改成用下划线“_”省略
-
定义&初始化
// 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 -
取值 & 删除
age := dict["张三"] // 若键不存在,返回零值 age, exists := dict["张三"] // exists 布尔类型,表示值是否存在 delete(dict, "张三") // 若map中存在对应key,删除之 -
赋值采用引用传递
-
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]) } -
map的键类型可以是任意值类型,内置 or 结构类型,必须能用
==比较。而map、slice、函数、及含有切片的结构类型等引用类型则不能作为键
-
基础类型:包括整型、浮点型、字符型、布尔型, 赋值采用值传递。对它们的操作一般返回一个新创建的值,所以是线程安全的。
-
引用类型:包括map、slice、chan、函数类型、接口类型,采用引用传递,本质是传递了底层的指针值(注意slice结构中还包含了len和cap两个基本类型,不会在函数中被修改)
-
结构类型
- 采用值传递,但结构体内的引用类型采用引用传递(本质是指针的值传递)
- 若要修改结构体值,应传递指针
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中一律用句号“.”访问成员 } -
自定义类型:定义结构体 或 基于已有类型
-
Go是强类型语言。即使底层类型相同,如果类型名不同也不能相互赋值
type Duration int64 var dur Duration dur = int64(100) // Error fmt.Println(dur) -
基于已有类型重新定义类型的好处
- 添加方法
- 明确业务含义
-
-
函数
- 函数定义中没有接收者
-
方法
-
方法定义中有接收者
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() // 两种均可
-
-
大小写
- 函数、方法、变量名、类型的首字母若大写,则可在包外使用,相当于java的public关键字
-
多值返回
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) } -
可变参数
- 函数入参表类型前加
...,必须是最后一个入参。 函数内该参数相当于一个数组
func main() { print("1","2","3") } func print (a ...interface{}){ for _,v:=range a{ fmt.Print(v) } fmt.Println() } - 函数入参表类型前加
-
接口是抽象的,仅包含一组接口方法。具体实现由用户实现
-
如果用户定义的类型(定义为
实体类型),实现了接口类型声明的所有方法,那么这个用户定义的类型就实现了这个接口 -
多态:实体类型对象可以赋值给对应的接口类型对象。那么在调用接口类型对象的方法时,将转化成对具体实体类型方法的调用
-
接口类型被赋值后,包含两个指针。第一个指针指向存储的实体类型的信息和相关联的方法集,第二个指针指向存储的实体类型的值
-
方法集
-
值接收的 方法,实体类型的值和指针都可以实现对应的接口(指针传递不会被改变值);而若采用指针接收, 只有实体类型的指针能够实现对应的接口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
-
-
示例
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() } -
Go语言中没有继承的概念。Go提倡的代码组合方式是组合。嵌入的为内部类型,包含的为外部类型
-
性质
- 嵌入后,内部类型的成员便也成为外部类型的成员
- 在外部类型中可以对内部类型的方法进行重写。但内部类型中的方法依然存在,可以通过内部类型去调用
- 如果内部实现了某个接口,则外部类型也实现了该接口
-
示例
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() } -
范围:变量、函数、类型、成员(变量/函数)
-
通过首字母大小写定义可见性,大写exported,小写unexported
-
.操作符前面的部分导出了,.操作符后面的部分才有可能被访问;如果.前面的部分都没有导出,那么即使.后面的部分是导出的,也无法访问例子 可否访问 Admin.User.Name 是 Admin.User.name 否 Admin.user.Name 否 Admin.user.name 否
-
基本概念
概念 说明 进程 一个程序对应的一个独立程序空间 线程 一个执行空间,一个进程可以有多个线程 逻辑处理器 执行创建的goroutine,绑定一个线程 调度器 Go运行时中的,分配goroutine给不同的逻辑处理器 全局运行队列 所有刚创建的goroutine都会放到这里 本地运行队列 逻辑处理器的goroutine队列 -
go语言中并发指的是让某个函数独立于其他函数运行的能力,一个goroutine就是一个独立的工作单元,Go的runtime(运行时)会在逻辑处理器上调度这些goroutine来运行,一个逻辑处理器绑定一个操作系统线程,所以说goroutine不是线程,它是一个协程,也是这个原因,它是由Go语言运行时本身的算法实现的
-
当我们创建一个goroutine的后,会先存放在
全局运行队列中,等待Go运行时的调度器进行调度,把他们分配给其中的一个逻辑处理器,并放到这个逻辑处理器对应的本地运行队列中,最终等着被逻辑处理器执行即可 -
设置逻辑处理器个数等于物理核数
runtime.GOMAXPROCS(runtime.NumCPU())
-
当两个或多个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 } } -
检查data race的方式
go run -race main.go go build -race // then run the binary generated -
避免并发的方式
// import "sync.atomic" atomic.AddInt32(&count, 1) // import "sync" var mu sync.Mutex // 互斥锁 mu.Lock() mu.Unlock() // 使用channel
-
定义 & 基本使用
// 双向通道 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是否已关闭 -
无缓冲通道要求发送和接收端都需要做好准备,否则一方将阻塞,等待另一方处理,所以又称同步通道。缓冲通道发送后并不要求立马接收
-
只有双向通道才能关闭,且只能关闭一次。重复关闭channel或往关闭了的channel发送数据会panic
-
并发示例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) } } -
资源共享池 -> 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 } -
补充:原生资源池 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} }
-
原因:读 - 读不互斥,读 - 写、写 - 写操作才互斥。直接采用sync.Mutex效率较低
-
示例
var mu sync.RWMutex // 读锁 mu.RLock() mu.RUnlock() // 写锁 mu.Lock() mu.Unlock() -
示例 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{}), } }
-
仅针对
interface{}类型。常见的如将输入传入func foo(interface{}), 参数自动转为interface{}类型 -
示例
// 直接断言使用 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 } -
转换类型的时候如果是string可以不用断言,使用fmt.Sprint()函数可以达到想要的效果
-
示例
// 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提供的标准抬头信息 ) -
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) } -
分级调用示例代码 → 比较麻烦,可以考虑第三方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("欢迎关注留言") } -
log获取文件信息的主要函数
runtime.Caller, 参数skip表示跳过栈帧数,0表示不跳过,也就是runtime.Caller的调用者。1的话就是再向上一层,表示调用者的调用者func Caller(skip int) (pc uintptr, file string, line int, ok bool)
-
接口定义
- 把数据的输入和输出,抽象为流的读写,所以只要实现了这两个接口,都可以使用流的读写功能
// 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) }
-
场景
-
等待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) }
-
-
接口定义
type Context interface { Deadline() (deadline time.Time, ok bool) // 获取截止时间 Done() <-chan struct{} // 取消channel如果可读,则说明发起了取消请求 Err() error Value(key interface{}) interface{} // 获取保存的Key-Value中的Value } -
基本的两个空Context:
context.Background()(主要用于main函数、初始化及测试代码)和context.TODO()。 二者不可取消、无deadline、无key-value -
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 // 不返回取消函数 -
使用原则
- 显式使用context,而不是放在struct里
- 向函数传递context时,应作为第一个参数,且不应传递nil context(可以传递
context.TODO(). - context的Value仅用于传递request-scoped data(进程、API等),而不是传递非必要数据
- context是并发安全的,可以同时传递给多个goroutine
-
示例
// 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") } } -
基本要求
- 含有单元测试代码的go文件必须以
_test.go结尾,Go语言测试工具只认符合这个规则的文件 - 单元测试文件名
_test.go前面的部分最好是被测试的方法所在go文件的文件名,比如例子中是main_test.go,因为测试的Add函数,在main.go文件里 - 单元测试的函数名必须以
Test开头,是可导出公开的函数 - 测试函数的签名必须接收一个指向
testing.T类型的指针,并且不能返回任何值 - 函数名最好是Test+要测试的方法函数名,比如例子中是
TestAdd,表示测试的是Add这个这个函数
- 含有单元测试代码的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")
}
sum = Add(3,4)
if sum == 7 {
t.Log("the result is ok")
} else {
t.Fatal("the result is wrong")
}
}
模拟调用 - 网络
-
单元测试的原则,就是所测方法不要受到所依赖环境的影响。所以对于联网等场景,需要进行模拟调用
-
标准库中提供了127.0.0.1:10000/debug/pprof/就能看到监控的一些信息了 // 可视化界面(需安装graphviz)的两种方法 go tool pprof localhost:10000/debug/pprof/profile go tool pprof -localhost:10000/debug/pprof/profile
-
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) -
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方法 -
文件复制示例
可以使用
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语言中局部变量声明后必须使用,否则报错。全局变量无此限制+整数、浮点数、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
)
语言结构
- 一行只有一条语句时可省略分号
// 当前程序的包名
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!")
}
运算符
- 算术:
+ - * / % ++(自增) --(自减) - 关系:
> >= < <= == != - 逻辑:
&& || ! - 位:
& | ^ << >>(^做一元运算符为位取反,二元为异或) - 赋值运算符 := 以及算术、位运算符结合=的赋值运算符
- 其他:&(取地址运算符)、* (取对应地址值)
-
声明和初始化
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)} -
数组采用值传递,需满足大小和类型相同才能赋值。若需要在函数中修改,用数组指针传参
func main() { array := [5]int{1, 2, 3} modify(&array) } func modify(arr *[5]int) { // 注意区别于指针数组类型 [5]*int arr[1] = 5 }
-
定义&初始化
// 定义 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{} // 空切片,指向底层数组的指针为一个地址 -
底层采用数组存储,包含3个字段:
- 指向底层数组的指针
- 切片长度
- 切片容量(仅用于扩展容量,超出长度的部分不能用于索引)
-
可通过内置函数
len()和cap()获取切片长度和容量 -
赋值为引用传递,此时对切片元素的修改会影响原切片。可用内置的
append()对赋值的变量追加元素,若此时底层数组容量足够,则直接对元素进行覆盖(但原切片len和cap不变)。否则新建一个底层数组. -
数组和切片迭代
for i := 0; i < len(slice); i++ {} for idx, val := range slice {} // idx 可改成用下划线“_”省略
-
定义&初始化
// 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 -
取值 & 删除
age := dict["张三"] // 若键不存在,返回零值 age, exists := dict["张三"] // exists 布尔类型,表示值是否存在 delete(dict, "张三") // 若map中存在对应key,删除之 -
赋值采用引用传递
-
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]) } -
map的键类型可以是任意值类型,内置 or 结构类型,必须能用
==比较。而map、slice、函数、及含有切片的结构类型等引用类型则不能作为键
-
基础类型:包括整型、浮点型、字符型、布尔型, 赋值采用值传递。对它们的操作一般返回一个新创建的值,所以是线程安全的。
-
引用类型:包括map、slice、chan、函数类型、接口类型,采用引用传递,本质是传递了底层的指针值(注意slice结构中还包含了len和cap两个基本类型,不会在函数中被修改)
-
结构类型
- 采用值传递,但结构体内的引用类型采用引用传递(本质是指针的值传递)
- 若要修改结构体值,应传递指针
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中一律用句号“.”访问成员 } -
自定义类型:定义结构体 或 基于已有类型
-
Go是强类型语言。即使底层类型相同,如果类型名不同也不能相互赋值
type Duration int64 var dur Duration dur = int64(100) // Error fmt.Println(dur) -
基于已有类型重新定义类型的好处
- 添加方法
- 明确业务含义
-
-
函数
- 函数定义中没有接收者
-
方法
-
方法定义中有接收者
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() // 两种均可
-
-
大小写
- 函数、方法、变量名、类型的首字母若大写,则可在包外使用,相当于java的public关键字
-
多值返回
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) } -
可变参数
- 函数入参表类型前加
...,必须是最后一个入参。 函数内该参数相当于一个数组
func main() { print("1","2","3") } func print (a ...interface{}){ for _,v:=range a{ fmt.Print(v) } fmt.Println() } - 函数入参表类型前加
-
接口是抽象的,仅包含一组接口方法。具体实现由用户实现
-
如果用户定义的类型(定义为
实体类型),实现了接口类型声明的所有方法,那么这个用户定义的类型就实现了这个接口 -
多态:实体类型对象可以赋值给对应的接口类型对象。那么在调用接口类型对象的方法时,将转化成对具体实体类型方法的调用
-
接口类型被赋值后,包含两个指针。第一个指针指向存储的实体类型的信息和相关联的方法集,第二个指针指向存储的实体类型的值
-
方法集
-
值接收的 方法,实体类型的值和指针都可以实现对应的接口(指针传递不会被改变值);而若采用指针接收, 只有实体类型的指针能够实现对应的接口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
-
-
示例
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() } -
Go语言中没有继承的概念。Go提倡的代码组合方式是组合。嵌入的为内部类型,包含的为外部类型
-
性质
- 嵌入后,内部类型的成员便也成为外部类型的成员
- 在外部类型中可以对内部类型的方法进行重写。但内部类型中的方法依然存在,可以通过内部类型去调用
- 如果内部实现了某个接口,则外部类型也实现了该接口
-
示例
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() } -
范围:变量、函数、类型、成员(变量/函数)
-
通过首字母大小写定义可见性,大写exported,小写unexported
-
.操作符前面的部分导出了,.操作符后面的部分才有可能被访问;如果.前面的部分都没有导出,那么即使.后面的部分是导出的,也无法访问例子 可否访问 Admin.User.Name 是 Admin.User.name 否 Admin.user.Name 否 Admin.user.name 否
-
基本概念
概念 说明 进程 一个程序对应的一个独立程序空间 线程 一个执行空间,一个进程可以有多个线程 逻辑处理器 执行创建的goroutine,绑定一个线程 调度器 Go运行时中的,分配goroutine给不同的逻辑处理器 全局运行队列 所有刚创建的goroutine都会放到这里 本地运行队列 逻辑处理器的goroutine队列 -
go语言中并发指的是让某个函数独立于其他函数运行的能力,一个goroutine就是一个独立的工作单元,Go的runtime(运行时)会在逻辑处理器上调度这些goroutine来运行,一个逻辑处理器绑定一个操作系统线程,所以说goroutine不是线程,它是一个协程,也是这个原因,它是由Go语言运行时本身的算法实现的
-
当我们创建一个goroutine的后,会先存放在
全局运行队列中,等待Go运行时的调度器进行调度,把他们分配给其中的一个逻辑处理器,并放到这个逻辑处理器对应的本地运行队列中,最终等着被逻辑处理器执行即可 -
设置逻辑处理器个数等于物理核数
runtime.GOMAXPROCS(runtime.NumCPU())
-
当两个或多个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 } } -
检查data race的方式
go run -race main.go go build -race // then run the binary generated -
避免并发的方式
// import "sync.atomic" atomic.AddInt32(&count, 1) // import "sync" var mu sync.Mutex // 互斥锁 mu.Lock() mu.Unlock() // 使用channel
-
定义 & 基本使用
// 双向通道 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是否已关闭 -
无缓冲通道要求发送和接收端都需要做好准备,否则一方将阻塞,等待另一方处理,所以又称同步通道。缓冲通道发送后并不要求立马接收
-
只有双向通道才能关闭,且只能关闭一次。重复关闭channel或往关闭了的channel发送数据会panic
-
并发示例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) } } -
资源共享池 -> 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 } -
补充:原生资源池 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} }
-
原因:读 - 读不互斥,读 - 写、写 - 写操作才互斥。直接采用sync.Mutex效率较低
-
示例
var mu sync.RWMutex // 读锁 mu.RLock() mu.RUnlock() // 写锁 mu.Lock() mu.Unlock() -
示例 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{}), } }
-
仅针对
interface{}类型。常见的如将输入传入func foo(interface{}), 参数自动转为interface{}类型 -
示例
// 直接断言使用 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 } -
转换类型的时候如果是string可以不用断言,使用fmt.Sprint()函数可以达到想要的效果
-
示例
// 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提供的标准抬头信息 ) -
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) } -
分级调用示例代码 → 比较麻烦,可以考虑第三方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("欢迎关注留言") } -
log获取文件信息的主要函数
runtime.Caller, 参数skip表示跳过栈帧数,0表示不跳过,也就是runtime.Caller的调用者。1的话就是再向上一层,表示调用者的调用者func Caller(skip int) (pc uintptr, file string, line int, ok bool)
-
接口定义
- 把数据的输入和输出,抽象为流的读写,所以只要实现了这两个接口,都可以使用流的读写功能
// 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) }
-
场景
-
等待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) }
-
-
接口定义
type Context interface { Deadline() (deadline time.Time, ok bool) // 获取截止时间 Done() <-chan struct{} // 取消channel如果可读,则说明发起了取消请求 Err() error Value(key interface{}) interface{} // 获取保存的Key-Value中的Value } -
基本的两个空Context:
context.Background()(主要用于main函数、初始化及测试代码)和context.TODO()。 二者不可取消、无deadline、无key-value -
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 // 不返回取消函数 -
使用原则
- 显式使用context,而不是放在struct里
- 向函数传递context时,应作为第一个参数,且不应传递nil context(可以传递
context.TODO(). - context的Value仅用于传递request-scoped data(进程、API等),而不是传递非必要数据
- context是并发安全的,可以同时传递给多个goroutine
-
示例
// 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") } } -
基本要求
- 含有单元测试代码的go文件必须以
_test.go结尾,Go语言测试工具只认符合这个规则的文件 - 单元测试文件名
_test.go前面的部分最好是被测试的方法所在go文件的文件名,比如例子中是main_test.go,因为测试的Add函数,在main.go文件里 - 单元测试的函数名必须以
Test开头,是可导出公开的函数 - 测试函数的签名必须接收一个指向
testing.T类型的指针,并且不能返回任何值 - 函数名最好是Test+要测试的方法函数名,比如例子中是
TestAdd,表示测试的是Add这个这个函数
- 含有单元测试代码的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")
}
sum = Add(3,4)
if sum == 7 {
t.Log("the result is ok")
} else {
t.Fatal("the result is wrong")
}
}
模拟调用 - 网络
-
单元测试的原则,就是所测方法不要受到所依赖环境的影响。所以对于联网等场景,需要进行模拟调用
-
标准库中提供了127.0.0.1:10000/debug/pprof/就能看到监控的一些信息了 // 可视化界面(需安装graphviz)的两种方法 go tool pprof localhost:10000/debug/pprof/profile go tool pprof -localhost:10000/debug/pprof/profile
-
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) -
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方法 -
文件复制示例
可以使用
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]) //读多少、写多少 }

