卖油翁的故事告诉我们,熟能生巧;唯手熟尔。(主讲人是 golang 方面的大师)
曾表示过:先空杯心态,然后认真学习别人的长处,取长补短才能更进一步。
现在我要说,有先驱&大师指导,跟你自己看书底下练习效果相差非常多
全部跟着3位大师重新走了一遍,发现他所说不假;一本书&教程,Golang所有内容概括到了 。
以前那篇文章《Golang代码走廊》 只是掌握Go语法,这里导师教你的不仅仅是语法,是Golang的使用思维
如果你去听大师讲,大概视频全长 5个半小时,如果还要练习,那么直接 * 2;看我这篇文章,大概1个小时吧。 我看着这套教程的时候,其实语法已经非常熟悉了(从C++转到Go,并决心以后多用Go,少用C++),但是看完之后还是收获很多。下面是我的笔记,认真记录了练习的过程————希望可以帮到你。(有基础的话,懂的可以不必细看,直接快速带过去)
btw: 他讲解基础语法的时候,顺带把 golang 的标准库一起讲完了。。。(我专门把标准库拿出来写)
大师口气很大,说听他讲完后,你基本就能写大型项目,并且已经完全掌握了语言特性。(有意思)
忘了说,这一次和 代码走廊
那一次非常不同,那一次是非常认真,这里只是查漏补缺。
Hello 文件名和包名无关,大写开头函数是可导出的函数(被外部引用),字符串采用 uft8 编码而不是ascii码。 函数名(被调用的函数)也是 uft8编码。
1 2 3 4 5 6 7 package mainimport "fmt" func main () { fmt.Printf("Hello, world\n" ) }
目录讲解 gopath 是 go 语言执行 & 寻找源码的依据(而不是goroot)。 多个项目依赖时,记得要安装 go install
,而不是直接 go run
。
关于项目依赖这部分,可以看我前面的文章。
Go提倡一个项目一个GOPATH,但是也可以只使用一个GOPATH
但是多项目依赖时要记得先安装,并且 import 的路径始终是相对于 GOPATH/src而言的
go fmt
,前大括号必须在上一行。(IDE 可以自己整理环境)
文档 本机文档可以查看 godoc 包名 关键字
,例如 godoc fmt Printf
。
在线的可以查看官网 《golang.org》 ,同时可以查看源码。
直接用 IDE 吧,直接查看源码.
变量 Variables, Simple Types。
:=
相当于声明和定义,有点儿像动态语言。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package mainimport ( "fmt" ) func main () { var message string message = "hello, world" fmt.Println(message) }
const 常量,同时可以按组定义 (一次定义多个)。小写定义的变量,包外不可见。 自动重复上面的规则上一个枚举定义的规则:
1 2 3 4 5 6 7 8 9 10 11 12 package mainimport "fmt" const ( answer1 = iota *2 answer2 ) func main () { fmt.Println(answer1, answer2) }
普通变量的省略写法:
1 2 3 4 5 6 7 8 9 10 package mainimport "fmt" func main () { nine := uint64 (9 ) fmt.Printf("value: %d\n" , nine) }
字符串 把它当做 slice/容器操作,或者借助 strings
包工具。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package mainimport "fmt" func main () { str := "hello,world,hellowold" substr1 := str[1 :] substr2 := str[1 :2 ] fmt.Println(substr1) fmt.Println(substr2) for i, r := range str { fmt.Println(i, " : " , r) } }
并且单引号括起来的不转义,尽量使用双引号。
if语句 即便是 fmt.Println 其实也是多返回值的函数,一般可以用 inline if 去检查错误。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package mainimport ( "os" "fmt" ) func main () { if numbers, errors := fmt.Println("Hello" ); errors != nil { os.Exit(1 ) } else { fmt.Printf("%d bytes charactors printed.\n" , numbers) } }
但是注意 inline 条件判断定义的变量,作用域只在 if-else 块儿。
switch语句 条件太多了的情况,switch 判断起来可以直接不写变量;默认执行一路之后 break 出去。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package mainimport ( "os" "fmt" ) func main () { n, err := fmt.Println("HelloWorld" ) switch { case err != nil : os.Exit(1 ) case n == 0 : fmt.Println("0 bytes output" ) case n != 11 : fmt.Println("wrong number of characters" ) default : fmt.Println("ok;" ) } fmt.Printf("\n" ) }
fallthrough 可以不停执行下去,不过没有必要。因为 case 里面可以写多个枚举:
1 2 3 4 switch value { case 'a' ,'e' ,'i' ,'o' ,'u' : vowels++ default : cons++ }
for循环 没有while循环,但是多种for循环解决所有的问题。
死循环
带有判断的循环:
1 2 3 4 5 var counter int counter = 0 for counter < 10 { }
普通循环:
1 2 3 for counter := 0 ; counter <10 ; counter++ { }
第一部分和第三部分可以同时赋值多个变量,simultaneously。
函数定义 简单定义: (有参数,没有返回值)
1 2 3 4 5 6 7 8 func printer (msg string , whom string ) { fmt.Print("%s 2 %s\n" , msg, whom) } func printer (msg, whom string ) { fmt.Print("%s 2 %s\n" , msg, whom) }
有返回值: (单个或者多个)
1 2 3 4 func printer (msg string ) error { _, err := fmt.Println(msg) return err }
可以在函数内部定义一个 defer 处理错误,比如文件操作:
1 2 3 4 5 6 7 8 9 func fileOperat () error { f, err := os.Create("re.log" ) if err != nil { return err } defer f.Close() f.Write(msg) return err }
返回值也可以定义实例,只不过不返回内容了: (多返回值,则指定多个返回值实例)
1 2 3 4 func printer (msg string ) (e error) { _, e := fmt.Println(msg) return }
defer 的执行顺序和定义顺序相反:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package mainimport ( "fmt" ) func main () { printer("mine" ) } func printer (msg string ) error { defer fmt.Println("over" ) defer fmt.Print("?\n" ); _, err := fmt.Println(msg) return err }
不定参数 和其他语言一样,msgs...string
,然后当做 slice 去遍历操作。
放在最后一个参数位置。
数组和切片 用切片 slice 明显多过数组,不仅仅是因为速度,还因为切片更加灵活可变。
数组:
1 2 3 4 5 6 7 8 9 10 11 12 package mainimport ( "fmt" ) func main () { words := [4 ]string {"the" , "world" ,"is" ,"ours." } fmt.Print(words) }
除非你默认指定用传递指针,否则数组默认按照值拷贝传递。(修改的是副本) 函数参数 s []string
实际上传递的是 slice。
slice 可以看做底层数组的一个 window 窗口: 从起始位置偏移,然后取N个元素
并且 slice 默认传递的是引用,修改的直接就是底层数组. (没有扩容的情况下)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package mainimport ( "fmt" ) func changeStr (strs []string ) { for _, value := range strs { fmt.Print(value) } fmt.Println() strs[0 ] = "ur" } func main () { words := []string {"the" , "world" ,"is" ,"ours." } changeStr(words) changeStr(words) }
slice 也可以进行 [start:len]
截取操作等。 同时可以使用 make
进行初始化,指定长度,容量capacity。
1 2 3 4 words := make ([]string , 4 , 8 ) words[0 ] = "xxx" ... words[3 ] = "xxx"
这里有用的全局函数是 len()
, cap()
, append()
;容量不足时会拷贝 & 翻倍。
所以使用 append 之类的操作,类似的操作要小心。
越界操作会保运行时异常。(golang直接报错,终止程序)
slice默认是引用传递,也体现在赋值上;直接赋值(包括截取操作),其实是共享同一份底层数组。拷贝副本
可以使用 copy()
函数 copy(newWords, oldWords)
。
map 字典或者键值对。
动态分配:
1 2 3 dayMonths := make (map [string ]int ) dayMonths["Jan" ] = 31 dayMonths["Feb" ] = 28
或者栈上建立 map:
1 2 3 4 dayMonths := map [string ]int { "Jan" : 31 , "Feb" : 28 , }
不存在的元素会拿到一个 zero value,而不会报异常。当然也可以使用 ok pattern:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package mainimport ( "fmt" ) func main () { dayMonths := make (map [string ]int ) dayMonths["Jan" ] = 31 dayMonths["Feb" ] = 28 days, ok := dayMonths["Jans" ] if !ok { fmt.Println("Can't get days for Jan" ) } else { fmt.Printf("%d\n" , days) } }
遍历,以 key-value range 的形式,但是不是插入的顺序了:
1 2 3 for month, day := range dayMonths { fmt.Println(month, "has " , day, "days" ) }
使用全局函数 delete() 删除具体的 key-value:
1 delete (dayMonths, "Feb" )
不过不存在 && 删除多次,不会报错(删除不生效)。(delete 内部做了错误检查)
byte切片 io处理的时候,字节slice用的非常多。(而不是直接写 string)
1 func (f *File) Write (b []byte ) (n int , err error)
并且在编译时就会对转换类型进行检查:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package mainimport ( "os" "fmt" ) func main () { f, err := os.Open("te.txt" ) if err != nil { fmt.Println("Open File Error" ) os.Exit(1 ) } defer f.Close() bytes := make ([]byte , 128 ) n, err := f.Read(bytes) str := string (bytes) fmt.Println("read " , str, " : " , n, " bytes" ) }
直接打印 byte 的话得到的是数字。string 转换成 byte :
1 2 str := "test" f.Write([]byte (str))
错误异常 这里大师讲了,这样一段话:
函数返回 error 给主调函数,然后主调函数处理异常。
another way called panic and recover, but it extremly rarely used .
Go 的哲学是 handling errors returned by funcs。(而不是抛出异常 & 异常处理那一套)
使用 fmt 的情况,大致类似如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package mainimport ( "os" "fmt" ) func printer (msg string ) error { if msg == "" { return fmt.Errorf("Unwilling to print an empty string" ) } _, err := fmt.Printf("%s\n" , msg) return err } func main () { if err := printer("" ); err != nil { fmt.Printf("printer failed: %s\n" , err) } os.Exit(1 ) }
errors 包的情况:
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 import "errors" var ( errorEmptyString = errors.New("Unwilling to print an empty string" ); ) func printer (msg string ) error { if msg == "" { return errorEmptyString } _, err := fmt.Printf("%s\n" , msg) return err } func main () { if err := printer("" ); err != nil { if err == errorEmptyString { fmt.Printf("printer failed: %s\n" , err) } } os.Exit(1 ) }
最后,他强调一般情况下都返回 error 即可,除非造成了不可挽回的、致使程序终止的情况才使用 panic。
协程&chan chan 让 goroutine 具备互相通信的能力。
简单的案例 (不设置 chan 的大小,for 循环不断发送):
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 package mainimport "fmt" func emit (c chan string ) { words := []string {"the" , "quick" , "brown" , "fox" } for _, word := range words { c <- word } close (c) } func main () { wordChan := make (chan string ) go emit(wordChan) for word := range wordChan { fmt.Printf("%s " , word) } fmt.Print("\n" ) }
实际上从 chan 拿到内容的时候,可以检查一下是否关闭了:
1 word, isOpen := <- wordChan
如果发送完毕,不关闭 chan, 那么就会造成死锁, main 这里一直等待。
对于接收无所谓,但是对于发送,一定要注意关闭 chan 。
select语句 有 多个 chan
读写的时候,用 select 管理&监听读写就很方便了。 书写语法有点儿像 switch,需要外层 for 循环支持:
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" func emit (wordChan chan string , done chan bool ) { words := []string {"the" ,"quick" ,"brown" ,"fox" } i := 0 for { select { case wordChan <- words[i]: i++ if i == len (words) { i = 0 } case <-done: fmt.Println("Got done" ) close (done) return } } } func main () { wordChan := make (chan string ) doneChan := make (chan bool ) go emit(wordChan, doneChan) for i:= 0 ; i < 10 ; i++ { fmt.Printf("%s\n" , <-wordChan) } doneChan <- true }
可以看到 select 里面会对不同的 chan 的读写进行判断。 (而不能用 switch,因为select 才能监控 chan 的 io 读写)
如果子 routines 不关闭 chan,那么这里貌似也没有问题,因为主 routine 发送完毕就结束了,程序结束了。甚至可以在子 routine里面发送信息给主线程,然后主线程等着接收,它接收到了结束后再结束。
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" func emit (wordChan chan string , done chan bool ) { words := []string {"the" ,"quick" ,"brown" ,"fox" } i := 0 for { select { case wordChan <- words[i]: i++ if i == len (words) { i = 0 } case <-done: fmt.Println("Got done" ) close (done) return } } } func main () { wordChan := make (chan string ) doneChan := make (chan bool ) go emit(wordChan, doneChan) for i:= 0 ; i < 10 ; i++ { fmt.Printf("%s\n" , <-wordChan) } doneChan <- true }
chan读写超时 如果我等对方,对方一直不发送怎么办?傻等?设置一个超时机制吧:
select 里面再加一个分支,为超时等待分支。
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 package mainimport ( "time" "fmt" ) func emit (wordChan chan string , done chan bool ) { words := []string {"the" ,"quick" ,"brown" ,"fox" } i := 0 t := time.NewTimer(3 * time.Second) defer close (wordChan) for { select { case wordChan <- words[i]: i++ if i == len (words) { i = 0 } case <-done: fmt.Println("Got done" ) done <- true return case <-t.C: fmt.Println("tiemout" ) return } } } func main () { wordChan := make (chan string ) doneChan := make (chan bool ) go emit(wordChan, doneChan) for word := range wordChan { fmt.Println(word) } }
传递chan 如果 chan 传递的类型是 chan,那么就是 chan 中 chan了,一方往这个chan里面写,传递这个chan,然后另一方拿到这个 chan,再从chan里面读取内容。
传递 chan 是一种能力。
多个协程实现线程池 获取网页长度:
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 package mainimport ( "os" "fmt" "io/ioutil" "net/http" ) func getPage (url string ) (int , error) { resp, err := http.Get(url) if err != nil { return 0 , err } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err!= nil { return 0 , err } return len (body),nil } func main () { url := "http://www.baidu.com/" pageLen, err := getPage(url) if err != nil { fmt.Println("Get Page Err" ) os.Exit(1 ) } fmt.Printf("%s is length %d\n" , url, pageLen) }
如果要测试几个 url 可以遍历,像这样?
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 package mainimport ( "os" "fmt" "io/ioutil" "net/http" ) func getPage (url string ) (int , error) { resp, err := http.Get(url) if err != nil { return 0 , err } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err!= nil { return 0 , err } return len (body),nil } func main () { urls := []string { "http://www.baidu.com/" , "http://www.merlinblog.site/" , "http://www.commoncommonheart.com/" } for _, url := range urls { pageLen, err := getPage(url) if err != nil { fmt.Println("Get Page Err" ) os.Exit(1 ) } fmt.Printf("%s is length %d\n" , url, pageLen) } }
运行速度是很慢的,可以怎么改进呢?
顺序 getUrl 可以改成多个协程进行任务,把具体数字回写给主线程
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 package mainimport ( "fmt" "io/ioutil" "net/http" ) func getPage (url string ) (int , error) { resp, err := http.Get(url) if err != nil { return 0 , err } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err!= nil { return 0 , err } return len (body),nil } func getter (url string , size chan int ) { pageLen, err := getPage(url) if err == nil { size <- pageLen } } func main () { urls := []string { "http://www.baidu.com/" , "http://www.merlinblog.site/" , "http://www.commoncommonheart.com/" } sizeChan := make (chan int ) for _, url := range urls { go getter(url, sizeChan) } for i:=0 ; i<len (urls); i++ { fmt.Printf("%s is length %d\n" , urls[i], <-sizeChan) } }
同时开启去取得任务,确实快多了,但是似乎还可以更快: 直接把 getPage 任务交出去,开10个协程,具体谁执行,我不关心。也就是主线程只管起就行了,具体的工作全部封装了:
1 2 3 4 5 6 7 8 9 10 11 func main () { urlChan := make (chan string ) sizeChan := make (chan int ) for i:=0 ; i<10 ; i++ { go worker(urlChan, sizeChan) } urlChan <- url fmt.Println(<-sizeChan) }
不必关心哪个线程抢到了任务。
worker 工作线程封装全部细节:
1 2 3 4 5 6 7 8 9 10 11 func worker (urlChan chan string , size Chan int ) { for { url := <-urlChan length, err = getPage(url) if err == nil { sizeChan <- length } else { sizeChan <- 0 } } }
多个写,多个读 goroutines 的时候,通过 chan 还是很容易控制的:
生产者不断发送 url
消费者不断拿到 url 然后 getPage
(中间可能有些协程抢不到任务,但是这属于调度任务,后续再说)。 总之协程让同步&协作变得非常简单。
nilChan的应用 如果你关闭了 chan,那么你后续将永远不再能通过这个 chan 进行读写,但是赋值为nil表示当前不再接受读写,之后再把原来的值赋值回来,则可以进行读写了。
chan = nil
相当于暂时关闭,好处是避免了阻塞。give up transmitting.(读的chan不要再接收,写的chan不要在发送)
例如下面的: (用在 select 里面,可以暂停其他分支的执行,即当前停止读写)
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 package mainimport ( "time" "fmt" "math/rand" ) func reader (ch chan int ) { t := time.NewTimer(3 * time.Second) for { select { case i := <-ch: fmt.Println(i) case <-t.C: ch = nil } } } func writer (ch chan int ) { t := time.NewTimer(2 * time.Second) for { select { case ch<- rand.Intn(10 ): case <- t.C: ch = nil } } } func main () { ch := make (chan int ) go reader(ch) go writer(ch) time.Sleep(10 * time.Second) }
关闭chan close(chan)
可以用于同步和协作:
举个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package mainimport ( "fmt" "time" ) func printer (msg string , goChan chan bool ) { <- goChan fmt.Printf("%s\n" , msg) } func main () { goChan := make (chan bool ) for i:= 0 ; i< 10 ; i++ { go printer(fmt.Sprintf("printer: %d" , i), goChan) } time.Sleep(5 * time.Second) close (goChan) time.Sleep(5 * time.Second) }
10个协程都阻塞等待着接收main发送的信息,但是main就是不发送;所以所有的协程阻塞,不能往下执行; 然后当 main close的时候,它们才能继续执行。
或者让所有 goroutines 结束 :(接到这个消息后,程序流程结束)
1 2 3 4 5 6 7 8 for { select { case <- stopChan: return default : } }
这里等到消息可以是真的接收到或者,收到 close 消息。
总结一条,其实就是停止阻塞&&关闭 chan.
运行时Type 其实就是 interface{}
,编译时确定不了,只有运行时才知道具体的类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 package mainimport ( "fmt" ) func whatisThis (i interface {}) { fmt.Printf("%T\n" , i) } func main () { whatisThis(10 ) }
typeSwitch 一个可以判断类型的 switch 语句,大致语法如下: switch 实例.(type)
,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package mainimport ( "fmt" ) func whatisThis (i interface {}) { switch i.(type ) { case string : fmt.Println("a string" ) case int : fmt.Println("a integer" ) default : fmt.Printf("Don't know\n" ) } } func main () { whatisThis(10 ) }
类型断言 %v
可以打印任何类型。如果用 实例.(具体类型)
那么这就是 type assertion,类型断言。
1 fmt.Println("a integer" , i.(int ))
类型断言,如果失败会引发 panic:
1 2 case int : fmt.Println("a integer" , i.(string ))
接口 没有继承结构 && 类型体系,只有 type + method associated with them。(也没有友元之类的机制)
任何满足接口的类型,都可以被传递或者接收,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 type shuffler interface { Len() int Swap(i, j int ) } func shuffle (s shuffler) { } type intSlice []int func (is intSlice) Len () int { } func (is intSlice) Swap (i, j int ) { }
接口表明了一种能力,feature。(接口内的方法应该是大写开头的)
你不用去表明什么子类父类,oo的概念;只要你把某种类型的实例当做某接口实例去用,那么编译时就会去检查是否正确。(你可以故意把某个接口原型写错,发现编译时就会提醒你)
标准库到处都是使用接口的案例,比如 io,只要表明具有某种能力,那么就能实现读写
带缓冲的chan 有点儿像队列,或者 semaphore。buffer 只要还有内容,可以不必阻塞等待。(队列里没有内容,则要阻塞等待)
最经典的案例,就是生产者消费者了:
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 ( "time" "fmt" "math/rand" ) func worker (sema chan bool ) { <- sema fmt.Print("[" ) time.Sleep(time.Duration(rand.Intn(10 )) * time.Second) fmt.Print("]" ) sema <-true } func main () { sema := make (chan bool , 10 ) for i:= 0 ; i < 1000 ; i++ { go worker(sema) } for i:= 0 ; i< 10 ; i++ { sema <-true } time.Sleep(10 * time.Second) }
其实这里控制了 buffer 的数量,也就控制了最大竞争数量,换句话说,控制了同时运行的 worker 最大数量(资源池只有10个机会)。
除了队列(buffered chan 之外,还有 Mutex, atomic count 等同步手段),比如 sync/atomic,运行一个协程计数少一个,完成任务再把计数器增加回来。
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 package mainimport ( "time" "fmt" "math/rand" "sync/atomic" ) var ( runningCount int64 = 0 ) func worker (sema chan bool ) { atomic.AddInt64(&runningCount, 1 ) fmt.Printf("[ %d" , runningCount) time.Sleep(time.Duration(rand.Intn(10 )) * time.Second) fmt.Print("]" ) atomic.AddInt64(&runningCount, -1 ) } func main () { sema := make (chan bool , 10 ) for i:= 0 ; i < 1000 ; i++ { go worker(sema) } for i:= 0 ; i< 10 ; i++ { sema <-true } time.Sleep(10 * time.Second) }
可以看到用 sync/atomic 做手动控制不如直接使用 buffer chan 来控制最大资源数目。
自定义类型 使用 type 关键字,就可以定义自定义类型了,同时也可以为他 attach 一些方法。
更加面向对象
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 package mainimport ( "io/ioutil" "net/http" "fmt" ) type webPage struct { url string body []byte err error } func (w *webPage) get () { resp, err := http.Get(w.url) if err != nil { fmt.Println("get Page err" ) w.err = err return } defer resp.Body.Close() w.body, err = ioutil.ReadAll(resp.Body) if err != nil { w.err = err } } func main () { w := &webPage{url: "http://www.baidu.com/" } w.get() fmt.Printf("url: %s, %d length; error? %v\n" , w.url, len (w.body), w.err) }
这里用的 receiver 是 pointer 类型。
也可以为已有类型起别名,然后为他添加一些特殊方法,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package mainimport "fmt" type SumSlice []int func (s SumSlice) sum () int { sum := 0 for _, i := range s { sum += i } return sum } func main () { var s SumSlice = SumSlice{1 , 2 , 3 , 5 , 8 , 13 } fmt.Printf("%d\n" , s.sum()) }
但是有时候操作的时候可能需要强制转换,毕竟别名和原来的内容还是有区别的。 (内部嵌入接口的内容,可以直接当做本接口内的,直接使用)
多包引用 只要注意控制路径,那么引用别的包之后就可以使用它定义的 exported 内容。
golang 每次 import 一个包的时候会做一些初始化的工作:(需要自己去实现)
引入全局变量
全局函数 (一般用于初始化包内变量的工作)
注意大小写规则,一般包内的私有变量外部如果想访问,应该专门给它写一个Getter。
为什么 Go 一般不需要外部的 shell/makefile 来进行编译 & 依赖管理?
因为 golang 中的 import + Go tool chain 已经做好了编译和依赖管理,而且做得不做。
go install : 编译之后,装到 bin 目录 (go install 对应目录即可,不用指定到 xxx.go 文件) — src/hello, go install hello 即可 go build : 仅仅是编译,但是不安装到相应的目录 (如果你没有可执行文件,那么根本不用 install, 直接 go build 即可) go run : 直接针对 .go 文件(而不是 package 或者目录的名字),然后运行一个临时的可执行文件 go vet : 在 package 基础上检查运行时错误的(不用刻意编译运行之后才发现错误,直接 go vet 发现运行时检查) go test: 支持单元测试
编译的时候,自动根据 import 做好依赖处理&编译,同时也会在 pkg 目录生成目标平台的 .a 文件。
go 的编译问题,不用刻意去谈依赖,import 就解决了。(并且并行编译)
单元测试 主要是使用go test
,虽然实际生产上实际使用的是更加专业的框架。来简单看一下吧:(实际内容也不少,规则不少)。
这里比较有意思,老师故意写了一个带有 Bug 的程序,然后通过单元测试找到了问题
Golang 里面不太喜欢用 Getter 之类的方法,而直接写变量的名字。
主要代码如下:(正常运行)
然后写一个专门的单元测试: (T型,B型)
go test
然后报错了,因为 poem 里面只有一个 Stanzas。
(其实还有一个错误,那个统计原因字符的函数,没有考虑其他字符如逗号以及空格的情况)
Fatalf 会立即终止测试运行,而 Errorf 则不会。(一般使用 Fatalf 立即停止)
最后,所有的单元测试,都是以包为单位的: 写上 package 之后,直接引用 testing 即可。
标准库部分 这部分单独写吧,大师讲的非常不错。参考 《StdGolang专栏》
最后,大师露个脸 : )
Web开发 这位导师也是点到为止,但是不够详细。
参考:《WebGolang专栏》
Merlin 2018.3.11 大师讲课不忘黑一下其他语言,笑; 京东看了下在售的 golang 相关的书不值得购买