GO Lang Pro
延迟执行 defer (虽迟但到)
之前的代码处理错误都是简单的打印log.Fatal()
,但是有些情况是必须要处理错误问题的,比如读取文件出错需要关闭文件流等
这时候就需要我们延迟方法返回执行完错误处理再返回
go提供了defer
关键字,在普通方法或者函数调用前加上go会推迟执行,但是必定执行,即使方法调用了return,被defer
修饰的语句还是执行
如下代码,即使return了返回错误还是会执行defer修饰的语句,输出GoodBye
注意! defer
只能修饰方法或者函数调用
1 2 3 4 5 6 7 8 9 10 11 func main () { Socialize() } func Socialize () error { defer fmt.Println("GoodBye" ) fmt.Println("hello" ) return fmt.Errorf("I don't want to talk" ) }
错误恢复以及处理
这里以递归为例
GO的递归没给什么特别的关键字或者语法糖,和其他语言一样用就行了
打印目录树 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 func main () { PrintFireTree("/home/tr/go/src" ) } func PrintFireTree (dirPath string ) error { files,err := ioutil.ReadDir(dirPath) if err != nil { log.Fatal(err) } for _,file := range files{ if !file.IsDir(){ fmt.Println(" " +file.Name()) }else { fmt.Println("printing dir:" ,dirPath+"/" +file.Name()) err :=PrintFireTree(dirPath+"/" +file.Name()) if err != nil { return err } } } return nil }
错误处理和恢复
就如同上面那个递归一样,方法返回了error信息,每次调用都需要处理error信息,这样比较复杂,我们可以用更简洁的方式:panic
和recover
手动生成一个panic错误 panic("program going down")
如同其他语言一样,go在出错的时候也提供了一个stack trace
用于回溯出错的所有点
panic()
只能用于程序bug的时候,不能因为用户输入错误数据终止程序
panic()
执行的时候会停止程序,输出错误栈,但是如果用户调用了recover()
,那么panic()
就只会打印错误消息
要注意的是:一个方法内执行了panic()
再执行recover()
是无效的,因为recover只能在panic中执行,如果panic执行完了,那么轮不到revocer程序就终止了,所以要将recover()
方法放入一个函数,使用defer
修饰的函数,如例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 func main () { fmt.Println("running" ) Stop() fmt.Println("running" ) } func Stop () { defer DoRec() fmt.Println("paniced" ) panic ("stopping program" ) fmt.Println("after pannic" ) } func DoRec () { recover () }
这样可以改造我们之前的打印目录树的程序,将里面的error去掉,修改为panic,将error丢入panic:panic(err)
,随后恢复和报告错误
也同样适用于其他任何程序
1 2 3 4 5 6 7 8 9 10 11 func reportPanic () { errInterface := recover () if errInterface == nil { return } err,ok = errInterface.(error) if ok{ fmt.Println(err) } }
recover的问题
recover 会从panic状态中恢复过来,但是recover会恢复任何panic,这就会导致问题
recover会返回panic的值,那么我们可以判断这个值是不是一个error或者其他我们定义的,如果决定不恢复可以增加一个panic()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func reportPanic () { errInterface := recover () if errInterface == nil { return } err,ok = errInterface.(error) if ok{ fmt.Println(err) }else { panic (errInterface) } }
GO 并发执行
GO 并发执行,或许在web应用中最为常见,所以书上举例了web,这里同样
web
go的 net/http
库提供了http请求用于发送和请求http报文数据
http.Get
:发送Get请求
http.Response
:结构体响应
以下是一个请求多次的程序,顺序执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import ( "fmt" "io/ioutil" "log" "net/http" ) func main () { ResponseSize("https://example.com" ) ResponseSize("https://example.com" ) ResponseSize("https://example.com" ) ResponseSize("https://example.com" ) } func ResponseSize (url string ) { response,err := http.Get(url) if err != nil { log.Fatal(err) } defer response.Body.Close() body,err := ioutil.ReadAll(response.Body) if err != nil { log.Fatal(err) } fmt.Println(len (body)) }
并发 goroutines
上面的代码顺序请求,go提供了并发执行的方式,其实类似java的线程
只需要使用go
:go myFunction()
只能用于方法调用,作用相当于开启了一个线程
go认为主方法就是一个goroutine
,当主方法的goroutine
结束,进程就退出了,所以即使有其他的go routine
在执行,一旦main的结束了就会退出
所以要等待其他goroutine
执行完毕:channel
以下代码是修改后的结果,这里暂时不用channel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func main () { go ResponseSize("https://example.com" ) go ResponseSize("https://example.com" ) go ResponseSize("https://example.com" ) go ResponseSize("https://example.com" ) time.Sleep(2 *time.Second) } func ResponseSize (url string ) { response,err := http.Get(url) if err != nil { log.Fatal(err) } defer response.Body.Close() body,err := ioutil.ReadAll(response.Body) if err != nil { log.Fatal(err) } fmt.Println(len (body)) }
这并行执行的时候并不保证谁是优先的,需要用channel控制
go <func>
不可以配合return
channel
可以用于goruntine
的控制,传递参数
1 2 3 4 5 var myChannel chan float64 myChannel = make (chan float64 ) myChannel := make (chan float64 )
赋值:myChannel <- 3.14
取值:<-myChannel
取值和赋值是同步的一个操作,一次赋值对应一次取值,如果没有一次赋值,执行取值会阻塞直到下次赋值,简单的例子
1 2 3 4 5 6 7 8 func greeting (myChannel chan string ) { myChannel <- "hi" } func main () { myChannel := make (chan string ) go greeting(myChannel) fmt.Println(<-myChannel) }
利用channel 完成进程间的同步:当一个线程X
生成了一个channel
,将他传递给其他线程时,其他线程每次执行赋值操作,都需要X
线程运行才能继续执行,如果X
此时阻塞,被传递的线程也陷入阻塞态
使用channel 同步的例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import ( "fmt" "io/ioutil" "log" "net/http" ) func main () { size := make (chan Page) go ResponseSize("https://example.com" ,size) go ResponseSize("https://baidu.com" ,size) fmt.Println(<-size) fmt.Println(<-size) } type Page struct { Url string Size int } func ResponseSize (url string ,channel chan Page) { response,err := http.Get(url) if err != nil { log.Fatal(err) } defer response.Body.Close() body,err := ioutil.ReadAll(response.Body) if err != nil { log.Fatal(err) } p := Page{Url:url,Size:len (body)} channel <- p }
生产者消费者
目前所有的channel都是unbuffered
类型,即子线程send数据给channel后立即阻塞,直到这个channel的内容被取出
buffered channel
意思是,可以先往channel放一部分数据channel := make(chan string,5) // 这个channel可以放五个数据
且缓存的数据实际是队列模式,其他线程取数据每次都取最早放入的数据,先进先出
这时候channel <- "a"
不会阻塞子线程,直到执行了5次后channel满了才阻塞,这样就可以用来编写一个生产者消费者
同时为了防止主线程直接结束,主线程需要等子线程传递来消息才结束
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package mainimport ( "fmt" "time" ) func main () { fmt.Println("start test" ) channel := make (chan string , 3 ) endChan := make (chan string , 2 ) go producer(channel, endChan) go consumer(channel, endChan) fmt.Println(<-endChan, <-endChan) } func producer (channel chan string , endChan chan string ) { for i := 0 ; i < 10 ; i++ { time.Sleep(time.Second * 2 ) fmt.Println("producer made product" ) channel <- fmt.Sprint("product" , i) } endChan <- "producer end" } func consumer (channel chan string , endChan chan string ) { for i := 0 ; i < 10 ; i++ { time.Sleep(time.Second * 6 ) product := <-channel fmt.Println(product, "consumed" ) } endChan <- "consumer end" }
GO 的自动化测试
在给程序新增功能后,需要测试老的以往的功能是否正常,可以使用go提供的自动测试工具
假设有以下代码,用于英语环境下的连续物件... , ... and ..
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import ( "fmt" "strings" ) func main () { phrases := []string {"apple" ,"orange" ,"pie" } fmt.Println(JoinWithCommas(phrases)) } func JoinWithCommas (phrases []string ) string { result := strings.Join(phrases[:len (phrases)-1 ],", " ) result += " and " + phrases[len (phrases)-1 ] return result }
在新增功能之前,我们可以开始编写自动测试了,提供一组输入和输出,如果代码的结果不匹配则fail
自动文件的命名规则,如果主文件是join.go
那么测试文件:join_test.go
测试文件不一定要和被测文件一个包,但要是想测的东西是私有的,那么只能在一个包下
测试方法名必须Test
开头
执行测试文件:go test ...
被测文件必须包含_test.go
否则无法找到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import ( "testing" "fmt" ) func TestTwoElements (t *testing.T) { list := []string {"apple" ,"orange" } want := "apple and orange" got := JoinWithCommas(list) if got != want { t.Error(errorString(list,got,want)) } } func TestMoreElements (t *testing.T) { list := []string {"apple" ,"orange" ,"pear" } want := "apple, orange, and pear" got := JoinWithCommas(list) if got != want { t.Error(errorString(list,got,want)) } } func errorString (list []string ,got string ,want string ) string { return fmt.Sprintf("JoinWithCommas(%#v) = \"%s\", want \"%s\"" ,list,got,want) }
这是文件结构 go test join
自动去~/go/src
下面找join
包,执行里面的Test
开头的方法
默认的go test <...>
会测试全部,可以添加配置测试部分
go test <...> -v
: 查看测试详情
go test <...> -run Two
:测试名字里面带Two
的方法
表格驱动测试
测试的很多代码是重复的,我们可以生成一个表格,输入数据和期望数据的表格
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import ( "testing" "fmt" ) type testData struct { list []string want string } func TestJoinWithCommas (t *testing.T) { testList := []testData{ {list:[]string {"apple" },want:" and apple" }, {list:[]string {"apple" ,"orange" },want:"apple and orange" }, {list:[]string {"apple" ,"orange" ,"pear" },want:"apple, orange, and pear" }, } for _,test := range testList{ got := JoinWithCommas(test.list) if got != test.want { t.Error(errorString(test.list,got,test.want)) } } } func errorString (list []string ,got string ,want string ) string { return fmt.Sprintf("JoinWithCommas(%#v) = \"%s\", want \"%s\"" ,list,got,want) }
GO 函数传递
GO 支持first-class
函数,可以用于函数间传递,所谓first-class
函数,意思是go的函数是可以被赋值给变量,由变量调用
1 2 3 4 5 6 7 8 9 func main () { h := sayHi h() } func sayHi () { fmt.Println("hi from tr" ) }
定义函数接口 传递函数作为参数,函数作为参数的时候不用预先声明
1 2 3 4 5 6 7 8 9 10 func main () { double(sayHi) } func sayHi () { fmt.Println("hi from tr" ) } func double (hi func () ) { hi() hi() }
函数作为参数传递的时候,作为参数的这个函数格式必须符合定义的入参的函数的格式
1 2 3 4 5 6 7 8 9 10 11 12 13 func main () { var s1 func () string var s2 func (string ) s1 = sayHello s2 = sayHi s1() s2("tr" ) } func sayHi (name string ) {}func sayHello () string { return "hello" }
GO 的web(请求响应)
这章节其实主要讲的是net/http
包提供的响应http
请求的内容
先简要概述下服务器软件的作用,首先客户端通过url和端口的方式访问物理机,物理机接收tcp或者udp报文(内部是http报文),将报文拆解后交给对应端口的服务器程序,程序收到http报文,可以看到报文头的访问路径/hello
,找到对应的处理方法处理请求后封装http报文交给网络的下一次发出
这章能感受到作为后端服务,go比Java强的地方,Java相比太过笨重,从JavaEE到Spring生态,开发web是比较重的任务,go则更轻量快速,运行效率更高
先从一个简单的web应用开始
simple web app 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package mainimport ( "log" "net/http" ) func viewHandler (writer http.ResponseWriter,request *http.Request) { message :=[]byte ("Hello from web powered by go!" ) _,err := writer.Write(message) if err != nil { log.Fatal(err) } } func main () { http.HandleFunc("/hello" ,viewHandler) err := http.ListenAndServe("localhost:8080" ,nil ) log.Fatal(err) }
结合html页面
用Go来返回html页面,我觉得还是前后端分离更好,单纯用Go的高效性能负责后端请求处理
Go提供了template模板,读取html文件内容,插入我们的动态内容返回给客户端
模板内需要插入的数据用{{}}`来标识,称为一个`action`
>
> `{{.}}
插入任何数据都会显示
{{if .}} 内容 {{end}}
只有传递的数据为true才显示内容
{{range .}} 内容 {{.}} 其他内容 {{end}}
传递一个slice,会遍历slice显示这段模板内容,将单个数据插入
{{.Name}}
传递一个结构体,显示结构体的Name属性
注意:引入的包需要是 html/template
而不是text/template
后者会显示任何东西即使是一段js代码
guestbook.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 package mainimport ( "fmt" "html/template" "log" "net/http" "os" ) func main () { http.HandleFunc("/guestbook" , ViewHandler) http.HandleFunc("/guestbook/new" , NewHandler) http.HandleFunc("/guestbook/add" , AddHandler) err := http.ListenAndServe("0.0.0.0:8080" , nil ) Check(err) } type Guestbook struct { SignaureCount int Signatures []string } func ViewHandler (writer http.ResponseWriter, request *http.Request) { signatures := GetStrings("signatures.txt" ) guestbook := Guestbook{ SignaureCount: len (signatures), Signatures: signatures, } html, err := template.ParseFiles("view.html" ) Check(err) err = html.Execute(writer, guestbook) Check(err) } func NewHandler (writer http.ResponseWriter, req *http.Request) { htmlTemplate, err := template.ParseFiles("new.html" ) Check(err) err = htmlTemplate.Execute(writer, nil ) Check(err) } func AddHandler (writer http.ResponseWriter, request *http.Request) { signature := request.FormValue("signature" ) options := os.O_WRONLY | os.O_APPEND | os.O_CREATE file, err := os.OpenFile("signatures.txt" , options, os.FileMode(0600 )) Check(err) _, err = fmt.Fprintln(file, signature) Check(err) err = file.Close() Check(err) http.Redirect(writer, request, "/guestbook" , http.StatusFound) } func Check (err error) { if err != nil { log.Fatal(err) } } func GetStrings (fileName string ) []string { var lines []string file, err := os.Open(fileName) if os.IsNotExist(err) { return nil } Check(err) defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { lines = append (lines, scanner.Text()) } Check(scanner.Err()) return lines }
两个html页面,一个展示,一个带个form用于新增
view.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta http-equiv ="X-UA-Compatible" content ="IE=edge" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > Document</title > </head > <body > <h1 > Guest Book</h1 > <div > {{.SignaureCount}} total signatures <a href ="/guestbook/new" > Add ur new signature</a > </div > <div > {{range .Signatures}} <p > {{.}}</p > {{end}} </div > </body > </html >
new.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta http-equiv ="X-UA-Compatible" content ="IE=edge" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > Document</title > </head > <body > <h1 > add signature</h1 > <form action ="/guestbook/add" method ="post" > <div > <input type ="text" name ="signature" id ="" placeholder ="signature" > </div > <div > <input type ="submit" value ="submit" > </div > </form > </body > </html >
数据就存储在本地的文件内:signatures.txt