Golang 一、Go语言基础 1. Golang为何高效
Go语言拥有与Java一样的GC 机制,且有c++的指针,丰富的标准库,Go目前已经内置了大量的库
协程(goroutine)和信道(channel) :值得称赞的是,Go 为生成协程和使用信道提供了轻量级的语法。
语言层面支持并发 ,这个就是Go最大的特色,天生的支持并发。
不像以前我们要用Thread库 来新建线程,还要用线程安全的队列库来共享数据。
for i := 0 ; i < 10 ; i++ { go func (i int ) { fmt.Println(i) }(i) }
采用知乎上一位与底层硬件打交道的开发人员的话:
225 人赞同了该回答 好多答主都是做WEB这块的,可能会忽略一些特殊领域,比如我们这些做智能终端设备相关项目的。现在很多智能终端系统,例如我们常用的快递柜、取票机等。基本上操作系统都采用了Android或者Linux系统,硬件采用ARM主板和一堆的控制板。基本上甲方都需要加一些系统定制的内容,比如状态栏隐藏、外设驱动适配等,这些都是系统级,采用C/C++/Java完成内核和系统的定制,这个没什么说的。重点是对这一堆控制板的底层接口、逻辑控制和上层接口的实现。以前我们都是采用APP里的NDK来实现,大量代码耗费在Java和C++的相互调用上。后来直接采用C/C++直接编译为可执行文件通过远程调用提供给上层,开发效率有所提升。然而,甲方需求中很多新出现的内容开始让我们应接不暇。例如:采用JSON格式通讯,采用浏览器作为客户端调用底层,多个控制板需要并发同时控制。这些东西有的对于Java来说好弄,有的对于C++来说好弄,但是没有一个能同时方便的实现上述功能的,直到发现了Go。考虑到行业人员因素,像我们这种做平台和控制的软件工程师,大多对C和C++较为熟悉,转去学习Go做这种必要的后台程序较为方便。而且硬件平台又特殊在不能方便的部署各类服务器,用Java较为不便,用C/C++也不方便。即使采用Java和C++,又会存在交叉编译,以及一大堆依赖库的坑。而Go的标准库都基本具备了这些功能,同时又能合理的利用现有的C/C++代码。比较忙,写的比较粗略,大致意思就是这样。
发布于 2017-09-18
2. 一个基本的go程序 主函数入口要求:
必须是main包,package main
必须是main方法,func main()
文件名不一定是main.go,但一般习惯与main.go
package main import "fmt" func main () { fmt.Println("Hello World" ) }
golang支持自动类型推断的赋值,但不支持隐式的类型转换 ,别名和原有类型也不能隐式转换
func TestFibList (t *testing.T) { a := 1 b := 1 t.Log(a) for i := 0 ; i < 5 ; i++ { t.Log(" " , b) tmp := a a = b b = tmp + a } }
不支持指针运算
字符串是值类型,默认初始化是空字符串,而不是nil
a := 1 aPtr := &a var s string t.Log("*" + s + "*" )
3. 数组与切片
数组是固定大小的,而切片是可以伸缩 的数组。声明只是少写一个容量大小。
数组必须是相同维数且相同个数才能比较,每个元素相同才相等;而切片只能和nil比较
a := [...]int {1 , 2 , 3 , 4 } b := [...]int {1 , 3 , 2 , 4 } d := [...]int {1 , 2 , 3 , 4 } t.Log(a == b) t.Log(a == d) a := []int {1 , 2 , 3 , 4 } b := []int {1 , 2 , 3 , 4 } t.Log(a, b)
func TestArrayTravel (t *testing.T) { for i := 0 ; i < len (arr); i++ { t.Log(arr[i]) } for _, e := range arr { t.Log(e) } } func TestArraySection (t *testing.T) { arr := [...]int {1 , 2 , 3 , 4 , 5 } arr_sec := arr[3 :] t.Log(arr_sec) }
无非就是维护一个数组,扩容时重新分配然后挪过去,维护一个大小和容量。
s2 := make ([]int , 3 , 5 ) t.Log(len (s2), cap (s2)) s := []int {} for i := 0 ; i < 10 ; i++ { s = append (s, i) t.Log(len (s), cap (s)) }
func TestSliceShareMemory (t *testing.T) { year := []string {"Jan" , "Feb" , "Mar" , "Apr" , "May" , "Jun" , "Jul" , "Aug" , "Sep" , "Oct" , "Nov" , "Dec" } Q2 := year[3 :6 ] summer := year[5 :8 ] summer[0 ] = "Unknow" t.Log(Q2) t.Log(year) }
4. map与set实现 m3 := make (map [int ]int , 10 ) t.Logf("len m3=%d" , len (m3)) func TestTravelMap (t *testing.T) { m1 := map [int ]int {1 : 1 , 2 : 4 , 3 : 9 } for k, v := range m1 { t.Log(k, v) } } if v, ok := m1[3 ]; ok { t.Log(ok) t.Logf("Key 3's value is %d" , v) } else { t.Log(ok) t.Log("key 3 is not existing." ) }
func TestMapWithFunValue (t *testing.T) { m := map [int ]func (op int ) int {} m[1 ] = func (op int ) int { return op } m[2 ] = func (op int ) int { return op * op } m[3 ] = func (op int ) int { return op * op * op } t.Log(m[1 ](2 ), m[2 ](2 ), m[3 ](2 )) }
golang内置集合中没有set的实现,但可以用map[type]bool
来实现元素的唯一性
func TestMapForSet (t *testing.T) { mySet := map [int ]bool {} mySet[1 ] = true n := 3 if mySet[n] { t.Logf("%d is existing" , n) } else { t.Logf("%d is not existing" , n) } mySet[3 ] = true t.Log(len (mySet)) delete (mySet, 1 ) if mySet[n] { t.Logf("%d is existing" , n) } else { t.Logf("%d is not existing" , n) } }
5. 字符串 开始我们提到了string是个值类型,这点区别于其他语言,它不是引用或者是指针类型
string是只读的byte slice,len函数时返回它的字节数
func TestStringFn (t *testing.T) { s := "A,B,C" parts := strings.Split(s, "," ) for _, part := range parts { t.Log(part) } t.Log(strings.Join(parts, "-" )) } func TestConv (t *testing.T) { s := strconv.Itoa(10 ) t.Log("str" + s) if i, err := strconv.Atoi("10" ); err == nil { t.Log(10 + i) } }
6. 函数是一等公民
可以有多个返回值
所以参数都是值传递 ,slice、map、channel会有引用传递的错觉
函数可以作为变量值
可以作为参数和返回值
func (指针类型/结构类型,代表某接口/结构的方法) fun_name (ops ...int ) return_type {}
匿名函数
定义一个匿名函数。
定义一个匿名函数并调用执行
package mainimport "fmt" func main () { f1 := func (x, y int ) (z int ) { z = x + y return } fmt.Println(f1) fmt.Println(f1(5 , 6 )) f2 := func (x, y int ) (z int ) { z = x + y return }(7 , 8 ) fmt.Println(f2) func () { fmt.Println(9 + 10 ) }() }
7. 面向对象OOP
type Employee struct { Id string Name string Age int }
func (e *Employee) String () string { fmt.Printf("Address is %x" , unsafe.Pointer(&e.Name)) return fmt.Sprintf("ID:%s/Name:%s/Age:%d" , e.Id, e.Name, e.Age) } func TestStructOperations (t *testing.T) { e := Employee{"0" , "Bob" , 20 } e2 := new (Employee) fmt.Printf("e is %T\n" , e) fmt.Printf("Address is %x\n" , unsafe.Pointer(&e.Name)) fmt.Println("调用string函数" ) fmt.Printf(e.String()) } e is encap.Employee Address is c042068880 调用string 函数 Address is c042068880 ID:0 /Name:Bob/Age:20
接口的最佳使用实践:
倾向于使用小的接口定义,很多接口只包含一个方法
如果需要多个功能最好编写多个接口
只依赖于必要功能的最小接口
type Programmer interface { WriteHelloWorld(name string ) string } type GoProgrammer struct { name string } func (g *GoProgrammer) WriteHelloWorld (name string ) string { g.name = name return g.name } func TestClient (t *testing.T) { var p Programmer = new (GoProgrammer) t.Log(p.WriteHelloWorld("yyf" )) }
匿名类型嵌入,不是继承,因为如果内部是父类,外部是子类的话,就会产生问题。
父类定义的方法无法访问子类的数据方法
子类并没有继承父类的方法
不支持子类的替换
type Pet struct {} func (p *Pet) Speak () { fmt.Print("..." ) } func (p *Pet) SpeakTo (host string ) { p.Speak() fmt.Println(" " , host) } type Dog struct { Pet } func (d *Dog) Speak () { fmt.Print("Wang!" ) } func TestDog (t *testing.T) { dog := new (Dog) dog.SpeakTo("Chao" ) }
打印结果:dog调用自己里面pet的speakto方法,然后调用的是pet的speak方法,所以不是继承关系。
… Chao
Golang实现多态,传入接口,则作为参数时必须是个指向子类多态的指针
例如:goProg := &GoProgrammer{}
,goProg是GoProgrammer的指针类型。
type Code string type Programmer interface { WriteHelloWorld() Code } type GoProgrammer struct {} func (p *GoProgrammer) WriteHelloWorld () Code { return "fmt.Println(\"Hello World!\")" } type JavaProgrammer struct {} func (p *JavaProgrammer) WriteHelloWorld () Code { return "System.out.Println(\"Hello World!\")" } func writeFirstProgram (p Programmer) { fmt.Printf("%T %v\n" , p, p.WriteHelloWorld()) } func TestPolymorphism (t *testing.T) { goProg := &GoProgrammer{} javaProg := new (JavaProgrammer) writeFirstProgram(goProg) writeFirstProgram(javaProg) }
空接口可以表示任何类型,像java中的Object类 ,但是要通过断言 来判断类型。
func DoSomething (p interface {}) { if i, ok := p.(int ); ok { fmt.Println("Integer" , i) return } if s, ok := p.(string ); ok { fmt.Println("string" , s) return } fmt.Println("Unknow Type" ) } func TestEmptyInterfaceAssertion (t *testing.T) { DoSomething(10 ) DoSomething("10" ) }
8. 错误处理 Go与其他语言不同,没有异常机制,但是用errors来new一个错误实例
例如:errors.New("n should be not less than 2")
var LessThanTwoError = errors.New("n should be not less than 2" )var LargerThenHundredError = errors.New("n should be not larger than 100" )func GetFibonacci (n int ) ([]int , error) { if n < 2 { return nil , LessThanTwoError } if n > 100 { return nil , LargerThenHundredError } fibList := []int {1 , 1 } for i := 2 ; i < n; i++ { fibList = append (fibList, fibList[i-2 ]+fibList[i-1 ]) } return fibList, nil } func TestGetFibonacci (t *testing.T) { if v, err := GetFibonacci(1 ); err != nil { t.Error(err) } else { t.Log(v) } }
异常机制一般及早返回,避免代码嵌套
func GetFibonacci2 (str string ) { var ( i int err error list []int ) if i, err = strconv.Atoi(str); err != nil { fmt.Println("Error" , err) return } if list, err = GetFibonacci(i); err != nil { fmt.Println("Error" , err) return } fmt.Println(list) }
9. panic与recover
func TestPanicVxExit (t *testing.T) { defer func () { fmt.Println("执行了defer方法" ) }() fmt.Println("Start" ) panic (errors.New("Something wrong!" )) } 执行结果:抛出异常 func TestRecover (t *testing.T) { defer func () { if err := recover (); err != nil { fmt.Println("recovered from " , err) } }() fmt.Println("Start" ) panic (errors.New("Something wrong!" )) } 打印: Start recovered from Something wrong!
10. package引用
传统的方式是放到src目录下。举个栗子,你的GOPATH是D:\workspace\www\src\go_work,那么你的项目应该新建目录D:\workspace\www\src\go_work\project_01,D:\workspace\www\src\go_work\project_02。假设project_01内的结构是
main.go
file1
file1.go(packagefile1)
假设project_02内的结构是
main.go
file2
file1.go(packagefile2)
那么你在project_01内要想引用project_02的packagefile2,应该写importproject_02/file2
package seriesimport "fmt" func square (n int ) int { return n * n } func GetFibonacciSeries (n int ) []int { ret := []int {1 , 1 } for i := 2 ; i < n; i++ { ret = append (ret, ret[i-2 ]+ret[i-1 ]) } return ret } package clientimport ( "go_learning/ch15/series" "testing" ) func TestPackage (t *testing.T) { t.Log(series.GetFibonacciSeries(5 )) }
go get -u + github路径
,例如go get -u github.com/easierway/concurrent_map
顾名思义在main函数执行 时,会先将所有依赖的init方法执行一遍
package seriesimport "fmt" func init () { fmt.Println("init1" ) } func init () { fmt.Println("init2" ) } func square (n int ) int { return n * n } func GetFibonacciSeries (n int ) []int { ret := []int {1 , 1 } for i := 2 ; i < n; i++ { ret = append (ret, ret[i-2 ]+ret[i-1 ]) } return ret } package clientimport ( "fmt" "go_learning/ch15/series" "testing" ) func TestPackage (t *testing.T) { fmt.Println(series.GetFibonacciSeries(5 )) } init1 init2 [1 1 2 3 5 ]
有些时候我们并不需要把整个包都导入进来,仅仅是是希望它执行init()函数而已。 这个时候就可以使用 import _ 引用该包。即使用【import _ 包路径】只是引用该包,仅仅是为了调用init()函数,所以无法通过包名来调用包中的其他函数。
在框架中使用格为重要,一些依赖的init方法执行。例如beego框架中的run方法
package mainimport ( _ "BeegoDemo1/routers" "github.com/astaxie/beego" ) func main () { beego.Run() }
11. 文件操作 11.1 读操作
file , err := os.Open("d:/test.txt" ) if err != nil { fmt.Println("open file err=" , err) } defer file.Close() reader := bufio.NewReader(file) for { str, err := reader.ReadString('\n' ) if err == io.EOF { break } fmt.Printf(str) } fmt.Println("文件读取结束..." )
file := "d:/test.txt" content, err := ioutil.ReadFile(file) if err != nil { fmt.Printf("read file err=%v" , err) } fmt.Printf("%v" , string (content))
11.2 写操作 与读大同小异了,先写入缓冲区,再用writer来写入writer.Flush()文件。
创建输出流时,有几个文件打开模式
const ( O_RDONLY int = syscall.O_RDONLY O_WRONLY int = syscall.O_WRONLY O_RDWR int = syscall.O_RDWR O_APPEND int = syscall.O_APPEND O_CREATE int = syscall.O_CREAT O_EXCL int = syscall.O_EXCL O_SYNC int = syscall.O_SYNC O_TRUNC int = syscall.O_TRUNC )
os.OpenFile(filePath, os.O_WRONLY | os.O_TRUNC, 0666 ) file, err := os.OpenFile(filePath, os.O_WRONLY | os.O_APPEND, 0666 ) file, err := os.OpenFile(filePath, os.O_RDWR | os.O_APPEND, 0666 )
filePath := "d:/abc.txt" file, err := os.OpenFile(filePath, os.O_WRONLY | os.O_CREATE, 0666 ) if err != nil { fmt.Printf("open file err=%v\n" , err) return } defer file.Close()str := "hello,Gardon\r\n" writer := bufio.NewWriter(file) for i := 0 ; i < 5 ; i++ { writer.WriteString(str) } writer.Flush()
二、并发编程 1. 协程机制 进程、线程(内核级线程)、协程(用户级线程)
goroutine 和协程
本质上,goroutine 就是协程。 不同的是,Golang 在 runtime、系统调用等多方面对 goroutine 调度进行了封装和处理,当遇到长时间执行或者进行系统调用时,会主动把当前 goroutine 的CPU (P) 转让出去,让其他 goroutine 能被调度并执行,也就是 Golang 从语言层面支持了协程。
Golang 的一大特色就是从语言层面原生支持协程 ,在函数或者方法前面加 go关键字就可创建一个协程。
进程、线程、协程区别
对于 进程、线程 ,都是有内核进行调度 ,有 CPU 时间片的概念,进行 抢占式调度 (有多种调度算法)
对于 协程 (用户级线程),这是对内核透明的,比线程更加轻量级的存在 ,重点:是完全由用户程序进行调度控制的。 因为是由用户程序自己控制,那么就很难像抢占式调度那样做到强制的 CPU 控制权切换到其他进程/线程,通常只能进行 协作式调度 ,需要协程自己主动把控制权转让出去之后,其他协程才能被执行到。协程不是进程也不是线程,是一个特殊的函数 ,这个函数可以在某个地方挂起,并且可以重新在挂起处外继续运行。
Thread与Groutine 与内核线程的对应关系
Java Thread 是 1 : 1
Groutine 是 M : N 的关系(多对多)
好处:如果一比一的话,发生切换就是直接进行内核线程的切换,消耗比较大
系统调用;
读写channel;
gosched主动放弃,会将G扔进全局队列;
一个G如果发生I/O事件、阻塞 等事件时:
(1)协程运行时会有一个守护进程 ,它会去进行计数,记录每个协程处理器完成的协程数目,当一段时间发现Processor完成的数量没有变化时,会往协程的任务栈里面插入一个特殊的标记,当协程运行到非内联函数时就会读到这个标记,会把自己中断下来插到协程等待队列的队尾,然后切换成下一个的协程运行。
(2)当某个协程被系统中断了,例如I/O需要等待的时候,为了提高整体的并发度 ,Processor会移动到另一个可使用的系统线程上去(例如从M0移到M1)。 继续执行它所挂队列中的其他的协程,当被中断的协程被唤醒后,会把自己加到某个Processor的等待队列里或者全局等待队列中。 中断和切换时,会保存上下文信息,例如寄存器和堆栈的信息。
2. 访问共享内存,并发操作
锁机制 sync.Mutex
,与其他语言的锁机制是一样的。
当我们访问共享变量时,加锁机制,控制并发访问
func TestCounterThreadSafe (t *testing.T) { var mut sync.Mutex counter := 0 for i := 0 ; i < 5000 ; i++ { go func () { defer func () { mut.Unlock() }() mut.Lock() counter++ }() } time.Sleep(1 * time.Second) t.Logf("counter = %d" , counter) }
使用sync.WaitGroup
计数器可以执行完后进行通知,而不用强制等待固定时间
func TestCounterWaitGroup (t *testing.T) { var mut sync.Mutex var wg sync.WaitGroup counter := 0 for i := 0 ; i < 5000 ; i++ { wg.Add(1 ) go func () { defer func () { wg.Done() mut.Unlock() }() mut.Lock() counter++ }() } wg.Wait() t.Logf("counter = %d" , counter) }
3. CSP并发机制 描述了两个独立的并发实体通过共享的通讯 channel(管道)进行通信的并发模型。
golang中的channel独立于两个实体对象,两个实体之间是匿名的,这个就实现实体中间的解耦。
channel是有容量限制 的,并且是异步 的模型
channel内部有锁对象 来维护内部的同步机制。
同时channel也分阻塞 和非阻塞 两种,只是声明有细微差别,加上容量大小罢了。
retCh := make (chan string , 1 )
func service () string { time.Sleep(time.Millisecond * 50 ) return "Done" } func otherTask () { fmt.Println("1.working on something else." ) time.Sleep(time.Millisecond * 100 ) fmt.Println("3.Task is done." ) } func AsyncService () chan string { (1 )retCh := make (chan string ) (2 )retCh := make (chan string , 1 ) go func () { ret := service() fmt.Println("2.returned result." ) retCh <- ret fmt.Println("5.service exited." ) }() return retCh } func TestAsynService (t *testing.T) { retCh := AsyncService() otherTask() fmt.Println("4.In the channel is" , <-retCh) time.Sleep(time.Second * 1 ) } 打印结果: (1 )阻塞 (2 )非阻塞 1. working on something else . 1. working on something else .2. returned result. 2. returned result.3. Task is done. 5. service exited.4. In the channel is Done 3. Task is done. 5. service exited. 4. In the channel is Done
4. 多路选择和超时控制
golang提供select进行多种情况的选择,如果满足其中一个就执行,并且有默认的(都不满足)。
golang中多个并发程序的数据同步是采用通道来传输 ,超时设计是通过使用select和case语句,类似于switch和case,在每一个case里进行一个io操作,比如读或者写,在最后一个case里调用time包里的After方法,可以达到超时检测效果。
func service () string { (1 )不超时time.Sleep(time.Millisecond * 50 ) (2 )超时time.Sleep(time.Millisecond * 100 ) return "Done" } func TestSelect (t *testing.T) { select { case ret := <-AsyncService(): t.Log(ret) case <-time.After(time.Millisecond * 100 ): t.Error("time out" ) } }
5. channel的关闭和广播机制 close(ch)
通过close函数,对该channel进行关闭。
所有的 channel 接收者都会在 channel 关闭时,立刻从阻塞等待中返回且上述 ok 值为 false。 这个广播机制 常被利用,进⾏向多个订阅者同时发送信号。如:退出信号。
v, ok <-ch; ok
为 bool 值,true 表示正常接受,false 表示通道关闭
向关闭的 channel 发送数据,会导致 panic
例如:下面实现当数据传送完毕时,通过close(ch)
来关闭通道,通知其它从channel中获取数据的实体,当获取<-ch
完毕后,返回false时,自动结束。
func dataProducer (ch chan int , wg *sync.WaitGroup) { go func () { for i := 0 ; i < 10 ; i++ { ch <- i } close (ch) wg.Done() }() } func dataReceiver (ch chan int , wg *sync.WaitGroup) { go func () { for { if data, ok := <-ch; ok { fmt.Println(data) } else { break } } wg.Done() }() } func TestCloseChannel (t *testing.T) { var wg sync.WaitGroup ch := make (chan int ) wg.Add(1 ) dataProducer(ch, &wg) wg.Add(1 ) dataReceiver(ch, &wg) wg.Add(1 ) dataReceiver(ch, &wg) wg.Wait() }
6. 任务的取消
通过关闭channel,close(cancelChan)
来取消任务
通过close函数的广播机制,a := <-cancelChan
,每个协程都接收到零值,从而知道channel关闭,退出。
func isCancelled (cancelChan chan struct {}) bool { select { case a := <-cancelChan: fmt.Println("close函数,向channel发送了零值:" , a) return true default : return false } } func TestCancel (t *testing.T) { cancelChan := make (chan struct {}, 0 ) for i := 0 ; i < 5 ; i++ { go func (i int , cancelCh chan struct {}) { for { if isCancelled(cancelCh) { break } time.Sleep(time.Millisecond * 5 ) } fmt.Printf("协程%d Cancelled\n" , i) }(i, cancelChan) } cancel_2(cancelChan) time.Sleep(time.Second * 1 ) }
• 根 Context:通过 context.Background () 创建 • 子 Context:context.WithCancel(parentContext) 创建 • ctx, cancel := context.WithCancel(context.Background())
• 当前 Context 被取消时,基于他的子 context 都会被取消 • 接收取消通知 <-ctx.Done()
func isCancelled (ctx context.Context) bool { select { case <-ctx.Done(): return true default : return false } } func TestCancel (t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) for i := 0 ; i < 5 ; i++ { go func (i int , ctx context.Context) { for { if isCancelled(ctx) { break } time.Sleep(time.Millisecond * 5 ) } fmt.Println(i, "Cancelled" ) }(i, ctx) } cancel() time.Sleep(time.Second * 1 ) }
7. 常见的并发任务 7.1 只执行一次(单例模式) golang在sync提供sync.Once
这个对象来实现只执行一次的单例模式 场景。
type Singleton struct { data string } var singleInstance *Singletonvar once sync.Oncefunc GetSingletonObj () *Singleton { once.Do(func () { fmt.Println("Create Obj" ) singleInstance = new (Singleton) }) return singleInstance } func TestGetSingletonObj (t *testing.T) { var wg sync.WaitGroup for i := 0 ; i < 10 ; i++ { wg.Add(1 ) go func () { obj := GetSingletonObj() fmt.Printf("%X\n" , unsafe.Pointer(obj)) wg.Done() }() } wg.Wait() }
7.2 仅需任意任务完成 采用golang的channel的特点。当channel放入,就直接返回第一个放入的。
使用BuffedChannel 来存数据,避免其它的协程阻塞,造成协程一直阻塞,内存泄漏。
func TestFirstResponse (t *testing.T) { t.Log("Before:" , runtime.NumGoroutine()) t.Log(FirstResponse(t)) time.Sleep(time.Second * 1 ) t.Log("After:" , runtime.NumGoroutine()) } func FirstResponse (t *testing.T) string { numOfRunner := 10 ch := make (chan string , numOfRunner) for i := 0 ; i < numOfRunner; i++ { go func (i int ) { ret := runTask(i) ch <- ret }(i) } return <-ch } func runTask (id int ) string { time.Sleep(10 * time.Millisecond) return fmt.Sprintf("The result is from %d" , id) }
7.3 必须所有任务完成 在sync包下,有sync.WaitGroup
来实现全部执行完返回,但我们采用csp机制下的channel来实现
在上文中,我们直接修改返回条件,将channel中所以都获取出来再返回。
for i := 0 ; i < numOfRunner; i++ { go func (i int ) { ret := runTask(i) ch <- ret }(i) } finalRet := "" for j := 0 ; j < numOfRunner; j++ { finalRet += <-ch + "\n" } return finalRet
8. 对象池 还是基于golang中的channel来实现对象池技术,对buffedchannel进行存取操作,从而实现池化技术
type ObjPool struct { bufChan chan *ReusableObj } type ReusableObj struct { poolName string } func NewObjPool (numOfObj int ) *ObjPool { objPool := ObjPool{} objPool.bufChan = make (chan *ReusableObj, numOfObj) for i := 0 ; i < numOfObj; i++ { objPool.bufChan <- &ReusableObj{} } return &objPool } func (p *ObjPool) GetObj (timeout time.Duration) (*ReusableObj, error) { select { case ret := <-p.bufChan: return ret, nil case <-time.After(timeout): return nil , errors.New("time out" ) } } func (p *ObjPool) ReleaseObj (obj *ReusableObj) error { select { case p.bufChan <- obj: return nil default : return errors.New("overflow" ) } } func TestObjPool (t *testing.T) { pool := NewObjPool(10 ) for i := 0 ; i < 11 ; i++ { if v, err := pool.GetObj(time.Second * 1 ); err != nil { t.Error(err) } else { fmt.Printf("%T\n" , v) if err := pool.ReleaseObj(v); err != nil { t.Error(err) } } } fmt.Println("Done" ) }
9. sync.Pool 对象缓存 名字虽然是pool,但更像个对象的缓存catch,避免重复创建提高开销 ,用以缓存对象。
每一次GC会清除掉sync.pool
缓存的对象,所以对象的缓存有效期为下一次GC 之前。
• 适合于通过复用 ,降低复杂对象的创建和 GC 代价 • 会对共享变量进行获取,所以有锁的开销 • 生命周期受 GC 影响,不适合于做连接池 等,需自己管理生命周期 的资源的池化
总结:是否使用sync.Pool取决于锁的对象创建开销大,还是sync.Pool里面对象的开销大
func TestSyncPool (t *testing.T) { pool := &sync.Pool{ New: func () interface {} { fmt.Println("Create a new object." ) return 100 }, } v := pool.Get().(int ) fmt.Println(v) pool.Put(3 ) runtime.GC() v1, _ := pool.Get().(int ) fmt.Println(v1) } func TestSyncPoolInMultiGroutine (t *testing.T) { pool := &sync.Pool{ New: func () interface {} { fmt.Println("Create a new object." ) return 10 }, } pool.Put(100 ) pool.Put(100 ) pool.Put(100 ) var wg sync.WaitGroup for i := 0 ; i < 10 ; i++ { wg.Add(1 ) go func (id int ) { fmt.Println(pool.Get()) wg.Done() }(i) } wg.Wait() }
三、进阶与实战 1. 测试 1.1 单元测试 我们一开始就在使用单元测试,类似与Java中的junit。
格式是:
源码⽂文件以 _test 结尾:xxx_test.go
测试⽅方法名以 Test 开头:func TestXXX(t *testing.T) {…}
func TestSquare (t *testing.T) { inputs := [...]int {1 , 2 , 3 } expected := [...]int {1 , 4 , 9 } for i := 0 ; i < len (inputs); i++ { ret := square(inputs[i]) if ret != expected[i] { t.Errorf("input is %d, the expected is %d, the actual %d" , inputs[i], expected[i], ret) } } }
• Fail, Error : 该测试失败,该测试继续,其他测试继续执行 • FailNow, Fatal : 该测试失败,该测试中止,其他测试继续执行
func TestErrorInCode (t *testing.T) { fmt.Println("Start" ) t.Error("Error" ) fmt.Println("End" ) } func TestFailInCode (t *testing.T) { fmt.Println("Start" ) t.Fatal("Error" ) fmt.Println("End" ) }
func TestSquareWithAssert (t *testing.T) { inputs := [...]int {1 , 2 , 3 } expected := [...]int {1 , 4 , 9 } for i := 0 ; i < len (inputs); i++ { ret := square(inputs[i]) assert.Equal(t, expected[i], ret) } }
1.2. Benchmark性能测试 Benchmark是一种性能测试方法,格式是前后添加b.ResetTimer() 和 b.StopTimer()
,即可测试中间的代码性能
func BenchmarkConcatStringByBytesBuffer (b *testing.B) { elems := []string {"1" , "2" , "3" , "4" , "5" } b.ResetTimer() for i := 0 ; i < b.N; i++ { var buf bytes.Buffer for _, elem := range elems { buf.WriteString(elem) } } b.StopTimer() }
go test -bench=. -benchmem
2. 反射编程 2.1 常见api • reflect.TypeOf
返回为类型 (reflect.Type),再用kind获取类型 • 通过 kind 的来判断类型 • reflect.ValueOf
返回为值 (reflect.Value) • 可以从 reflect.Value
获得类型 reflect.ValueOf(f).Type()
func CheckType (v interface {}) { t := reflect.TypeOf(v) switch t.Kind() { case reflect.Float32, reflect.Float64: fmt.Println("Float" ) case reflect.Int, reflect.Int32, reflect.Int64: fmt.Println("Integer" ) default : fmt.Println("Unknown" , t) } }
func reflectTest01 (b interface {}) { rTyp := reflect.TypeOf(b) fmt.Println("rType=" , rTyp) rVal := reflect.ValueOf(b) n2 := 2 + rVal.Int() fmt.Println("n2=" , n2) fmt.Printf("rVal=%v rVal type=%T\n" , rVal, rVal) iV := rVal.Interface() num2 := iV.(int ) fmt.Println("num2=" , num2) iV := rVal.Interface() fmt.Printf("iv=%v iv type=%T \n" , iV, iV) stu, ok := iV.(Student) if ok { fmt.Printf("stu.Name=%v\n" , stu.Name) } }
2.2 通过反射调用方法 按名字访问结构的成员reflect.ValueOf(*e).FieldByName("Name")
按名字访问结构的⽅方法reflect.ValueOf(e).MethodByName("UpdateAge").Call([]reflect.Value{reflect.ValueOf(1)})
func TestInvokeByName (t *testing.T) { e := &Employee{"1" , "Mike" , 30 } reflect.ValueOf(e).MethodByName("UpdateAge" ). Call([]reflect.Value{reflect.ValueOf(1 )}) t.Log("Updated Age:" , e) }
注意:这二者的不同,前者是返回两个参数一个数据,一个bool值,后者是返回name属性的值。
reflect.TypeOf(*e).FieldByName("Name" ) reflect.ValueOf(*e).FieldByName("Name" )
2.3 反射解析Struct Tag type Employee struct { EmployeeID string Name string `firstname:"杨"` Age int } if nameField, ok := reflect.TypeOf(*e).FieldByName("Name" ); !ok { t.Error("Failed to get 'Name' field." ) } else { t.Log(nameField.Tag.Get("firstname" )) }
3. “不安全”编程Unsafe Unsafe类很少用,顾名思义不安全的类,一般用于外部c程序交互使用
注意:go里面原子类型操作必须使用Unsafe类。
atomic.StorePointer(&shareBufPtr, unsafe.Pointer(&data))
atomic.LoadPointer(&shareBufPtr)
func TestConvert (t *testing.T) { a := []int {1 , 2 , 3 , 4 } b := *(*[]MyInt)(unsafe.Pointer(&a)) t.Log(b) } func TestAtomic (t *testing.T) { var shareBufPtr unsafe.Pointer writeDataFn := func () { data := []int {} for i := 0 ; i < 100 ; i++ { data = append (data, i) } atomic.StorePointer(&shareBufPtr, unsafe.Pointer(&data)) } readDataFn := func () { data := atomic.LoadPointer(&shareBufPtr) fmt.Println(data, *(*[]int )(data)) } var wg sync.WaitGroup writeDataFn() for i := 0 ; i < 10 ; i++ { wg.Add(1 ) go func () { for i := 0 ; i < 10 ; i++ { writeDataFn() time.Sleep(time.Microsecond * 100 ) } wg.Done() }() wg.Add(1 ) go func () { for i := 0 ; i < 10 ; i++ { readDataFn() time.Sleep(time.Microsecond * 100 ) } wg.Done() }() } wg.Wait() }
4. 常见任务 4.1 JSON解析
主要原理是通过反射基于struct的Tag来识别json,所以效率低下
var jsonStr = `{ "basic_info":{ "name":"Mike", "age":30 }, "job_info":{ "skills":["Java","Go","C"] } } ` func TestEmbeddedJson (t *testing.T) { e := new (Employee) err := json.Unmarshal([]byte (jsonStr), e) if err != nil { t.Error(err) } fmt.Println(*e) if v, err := json.Marshal(e); err == nil { fmt.Println(string (v)) } else { t.Error(err) } }
import "encoding/json" func TestEasyJson (t *testing.T) { e := Employee{} e.UnmarshalJSON([]byte (jsonStr)) fmt.Println(e) if v, err := e.MarshalJSON(); err != nil { t.Error(err) } else { fmt.Println(string (v)) } }
使用benchmark进行性能比较:easyjson明显效率更高
BenchmarkEmbeddedJson-8 286228 3732 ns/op BenchmarkEasyJson-8 1000000 1029 ns/op
4.2 Http服务和Restful服务
func main () { http.HandleFunc("/" , func (w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello World!" ) }) http.HandleFunc("/time/" , func (w http.ResponseWriter, r *http.Request) { t := time.Now() timeStr := fmt.Sprintf("{\"time\": \"%s\"}" , t) w.Write([]byte (timeStr)) }) http.ListenAndServe(":8080" , nil ) }
使用 github.com/julienschmidt/httprouter
特点可以使用REST风格的API,使用get、post、delete方法
func Index (w http.ResponseWriter, r *http.Request, _ httprouter.Params) { fmt.Fprint(w, "Welcome!\n" ) } func Hello (w http.ResponseWriter, r *http.Request, ps httprouter.Params) { fmt.Fprintf(w, "hello, %s!\n" , ps.ByName("name" )) } func main () { router := httprouter.New() router.GET("/" , Index) router.GET("/hello/:name" , Hello) router.GET("/hello/:name" , Hello) log.Fatal(http.ListenAndServe(":8080" , router)) }
type Employee struct { ID string `json:"id"` Name string `json:"name"` Age int `json:"age"` } func GetEmployeeByName (w http.ResponseWriter, r *http.Request, ps httprouter.Params) { qName := ps.ByName("name" ) var ( ok bool info *Employee infoJson []byte err error ) if info, ok = employeeDB[qName]; !ok { w.Write([]byte ("{\"error\":\"Not Found\"}" )) return } if infoJson, err = json.Marshal(info); err != nil { w.Write([]byte (fmt.Sprintf("{\"error\":,\"%s\"}" , err))) return } w.Write(infoJson) }
5. 高效的性能代码 5.1 锁对性能的影响 总结:
• 减少锁的影响范围 • 减少发⽣生锁冲突的概率• 读多写少情况下sync.Map,甚至比ConcurrentMap性能好 • 读写频繁ConcurrentMap • 避免锁的使⽤用
func lockFreeAccess () { var wg sync.WaitGroup wg.Add(NUM_OF_READER) for i := 0 ; i < NUM_OF_READER; i++ { go func () { for j := 0 ; j < READ_TIMES; j++ { _, err := cache["a" ] if !err { fmt.Println("Nothing" ) } } wg.Done() }() } wg.Wait() } func lockAccess () { var wg sync.WaitGroup wg.Add(NUM_OF_READER) m := new (sync.RWMutex) for i := 0 ; i < NUM_OF_READER; i++ { go func () { for j := 0 ; j < READ_TIMES; j++ { m.RLock() _, err := cache["a" ] if !err { fmt.Println("Nothing" ) } m.RUnlock() } wg.Done() }() } wg.Wait() }
benchmark测试结果:加锁比不加锁多两个数量级
pkg: go1/go_learning/ch48/lock BenchmarkLockFree-8 206 6134092 ns/op BenchmarkLock-8 8 149600300 ns/op
5.2 加读/写锁、sync.Map、ConcurrentMap的性能比较 (1)读写情况下增加 读/写锁
type RWLockMap struct { m map [interface {}]interface {} lock sync.RWMutex } func (m *RWLockMap) Get (key interface {}) (interface {}, bool ) { m.lock.RLock() v, ok := m.m[key] m.lock.RUnlock() return v, ok } func (m *RWLockMap) Set (key interface {}, value interface {}) { m.lock.Lock() m.m[key] = value m.lock.Unlock() } func (m *RWLockMap) Del (key interface {}) { m.lock.Lock() delete (m.m, key) m.lock.Unlock() }
(2)读写情况下使用sync.Map
重点:适合读多写少 ,且 Key 相对稳定的环境,不经常修改
它原理是用空间换时间 来减少锁冲突,采用指针的方式间接实现值的映射 ,所以存储空间会较 built-in map ⼤
底层维护一个只读的空间,同样大小的写空间用读写锁
当读miss时会去dirty中找但是多读了一次还加了读写锁,所以性能不好,当miss的次数到一定量时,dirty区域就会更新到Read区域,底层是原子的CAS操作。
type SyncMapBenchmarkAdapter struct { m sync.Map } func (m *SyncMapBenchmarkAdapter) Set (key interface {}, val interface {}) { m.m.Store(key, val) } func (m *SyncMapBenchmarkAdapter) Get (key interface {}) (interface {}, bool ) { return m.m.Load(key) } func (m *SyncMapBenchmarkAdapter) Del (key interface {}) { m.m.Delete(key) }
(3)读写频繁建议使用ConcurrentMap
ConcurrentMap与Java一样,使用分段锁 ,比起使用读写锁锁住整个Map,它减少了锁的粒度
type ConcurrentMapBenchmarkAdapter struct { cm *concurrent_map.ConcurrentMap } func (m *ConcurrentMapBenchmarkAdapter) Set (key interface {}, value interface {}) { m.cm.Set(concurrent_map.StrKey(key.(string )), value) } func (m *ConcurrentMapBenchmarkAdapter) Get (key interface {}) (interface {}, bool ) { return m.cm.Get(concurrent_map.StrKey(key.(string ))) } func (m *ConcurrentMapBenchmarkAdapter) Del (key interface {}) { m.cm.Del(concurrent_map.StrKey(key.(string ))) }
BenchmarkSyncmap/map_with_RWLock-8 578 1952523 ns/op BenchmarkSyncmap/sync.map-8 686 1468380 ns/op BenchmarkSyncmap/concurrent_map-8 1377 885345 ns/op
6. GC友好的代码 6.1 复杂对象传引用 go语言类似与c语言时值传递,每次传递都会拷贝对象,所以对于一些复杂对象(数组、结构体)我们一般采用传引用的方式
func withReference(arr *[NumOfElems]Content) int
6.2 避免内存分配和复制 对于切片,我们有时不指定大小,就会频繁的分配内存和复制,导致GC
• 初始化至合适的大小 • 自动扩容是有代价的(切片) • 复用内存
func TestAutoGrow (t *testing.T) { for i := 0 ; i < times; i++ { s := []int {} for j := 0 ; j < numOfElems; j++ { s = append (s, j) } } } func TestProperInit (t *testing.T) { for i := 0 ; i < times; i++ { s := make ([]int , 0 , 100000 ) for j := 0 ; j < numOfElems; j++ { s = append (s, j) } } } func TestOverSizeInit (t *testing.T) { for i := 0 ; i < times; i++ { s := make ([]int , 0 , 800000 ) for j := 0 ; j < numOfElems; j++ { s = append (s, j) } } }
benchmark测试性能结果:很明显初始化容量大小后,复用内存更多,避免了扩容分配内存,执行次数更少
BenchmarkAutoGrow-8 2064 623091 ns/op BenchmarkProperInit-8 7076 178709 ns/op BenchmarkOverSizeInit-8 1737 651929 ns/op
6.3 高效的字符串拼接 与Java一样,go里面string也是不可变类型,所以+循环拼接效率很低,建议采用strings.Builder
String底层还是byte数组,所以可以使用bytes.Buffer一段连续的存储空间,可以往里面写,最后转成string类型。
func BenchmarkStringAdd (b *testing.B) { b.ResetTimer() for idx := 0 ; idx < b.N; idx++ { var s string for i := 0 ; i < numbers; i++ { s += strconv.Itoa(i) } } b.StopTimer() } func BenchmarkBytesBuf (b *testing.B) { b.ResetTimer() for idx := 0 ; idx < b.N; idx++ { var buf bytes.Buffer for i := 0 ; i < numbers; i++ { buf.WriteString(strconv.Itoa(i)) } _ = buf.String() } b.StopTimer() } func BenchmarkStringBuilder (b *testing.B) { b.ResetTimer() for idx := 0 ; idx < b.N; idx++ { var builder strings.Builder for i := 0 ; i < numbers; i++ { builder.WriteString(strconv.Itoa(i)) } _ = builder.String() } b.StopTimer() }
benchmark性能测试结果:
BenchmarkStringAdd-8 150411 7418 ns/op BenchmarkBytesBuf-8 750408 1648 ns/op BenchmarkStringBuilder-8 1085298 1189 ns/op
7. 网络Socket编程 golang里面的TCP编程还是类似于Java,使用Socket编程
func main () { conn, err := net.Dial("tcp" , "192.168.20.253:8888" ) if err != nil { fmt.Println("client dial err=" , err) return } reader := bufio.NewReader(os.Stdin) for { line, err := reader.ReadString('\n' ) if err != nil { fmt.Println("readString err=" , err) } line = strings.Trim(line, " \r\n" ) if line == "exit" { fmt.Println("客户端退出.." ) break } _, err = conn.Write([]byte (line + "\n" )) if err != nil { fmt.Println("conn.Write err=" , err) } } }
func process (conn net.Conn) { defer conn.Close() for { buf := make ([]byte , 1024 ) n , err := conn.Read(buf) if err != nil { fmt.Printf("客户端退出 err=%v" , err) return } fmt.Print(string (buf[:n])) } } func main () { fmt.Println("服务器开始监听...." ) listen, err := net.Listen("tcp" , "0.0.0.0:8888" ) if err != nil { fmt.Println("listen err=" , err) return } defer listen.Close() for { fmt.Println("等待客户端来链接...." ) conn, err := listen.Accept() if err != nil { fmt.Println("Accept() err=" , err) } else { fmt.Printf("Accept() suc con=%v 客户端ip=%v\n" , conn, conn.RemoteAddr().String()) } go process(conn) } }
8. Go操作Mysql数据库 Go 语言中的database/sql
包定义了对数据库的一系列操作。database/sql/driver
包定义了应被数据库驱动实现的接口 ,这些接口会被sql
包使用。
但是 Go 语言没有提供任何官方的数据库驱动,所以我们需要导入第三方的数据库驱动 。不过我们连接数据库之后对数据库操作的大部分代码都使用 sql 包。
8.1 获取数据库连接 1) 创建一个 db.go 文件,导入 database/sql 包以及第三方驱动包
package modelsimport ( "database/sql" _ "github.com/go-sql-driver/mysql" )
2) 定义两个全局变量,sql.DB后续增删改查中通用一个数据库操作句柄
var ( Db *sql.DB err error )
3) 创建 init 函数,在函数体中调用 sql 包的 Open 函数获取连接
func init () { Db, err = sql.Open("mysql" , "root:root@tcp(localhost:3306)/test" ) if err != nil { panic (err.Error()) } }
返回的 DB 可以安全的被多个 go 程同时使用,并会维护自身的闲置连接池。 这样一来,Open 函数只需调用一次。很少需要关闭 DB。
8.2 增删改查操作
type User struct { ID int Username string Password string Email string } func (user *User) AddUser () error { sqlStr := "insert into users(username,password,email) values(?,?,?)" inStmt, err := utils.Db.Prepare(sqlStr) if err != nil { fmt.Println("预编译出现异常:" , err) return err } _, err2 := inStmt.Exec("xxx" , "123456" , "xxx@qq.com" ) if err2 != nil { fmt.Println("执行出现异常:" , err2) return err } return nil } func (user *User) AddUser2 () error { sqlStr := "insert into users(username,password,email) values(?,?,?)" _, err := utils.Db.Exec(sqlStr, "admin2" , "666666" , "xxx@qq.com" ) if err != nil { fmt.Println("执行出现异常:" , err) return err } return nil }
9. Go操作Redis 9.1 基本命令使用 需要导入第三方包"github.com/garyburd/redigo/redis"
,就可以实现Redis的基本命令
先打开redis服务端
再启动main程序
func main () { conn, err := redis.Dial("tcp" , "127.0.0.1:6379" ) if err != nil { fmt.Println("redis.Dial err=" , err) return } defer conn.Close() _, err = conn.Do("HSet" , "user01" , "name" , "john" ) if err != nil { fmt.Println("hset err=" , err) return } _, err = conn.Do("HSet" , "user01" , "age" , 18 ) if err != nil { fmt.Println("hset err=" , err) return } r1, err := redis.String(conn.Do("HGet" ,"user01" , "name" )) if err != nil { fmt.Println("hget err=" , err) return } r2, err := redis.Int(conn.Do("HGet" ,"user01" , "age" )) if err != nil { fmt.Println("hget err=" , err) return } fmt.Printf("操作ok r1=%v r2=%v \n" , r1, r2) }
9.2 使用Redis连接池 还是使用那个第三方包,内部有redis连接池实现。
var pool *redis.Poolfunc init () { pool = &redis.Pool{ MaxIdle: 8 , MaxActive: 0 , IdleTimeout: 100 , Dial: func () (redis.Conn, error) { return redis.Dial("tcp" , "localhost:6379" ) }, } } func main () { conn := pool.Get() defer conn.Close() _, err := conn.Do("Set" , "name" , "汤姆猫~~" ) if err != nil { fmt.Println("conn.Do err=" , err) return } r, err := redis.String(conn.Do("Get" , "name" )) if err != nil { fmt.Println("conn.Do err=" , err) return } fmt.Println("r=" , r) conn2 := pool.Get() _, err = conn2.Do("Set" , "name2" , "汤姆猫~~2" ) if err != nil { fmt.Println("conn.Do err~~~~=" , err) return } r2, err := redis.String(conn2.Do("Get" , "name2" )) if err != nil { fmt.Println("conn.Do err=" , err) return } fmt.Println("r=" , r2) }