Golang: Here We Go(5.孰能生巧-Oreilly大师讲解&再练习)

卖油翁的故事告诉我们,熟能生巧;唯手熟尔。(主讲人是 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 main

import "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 main

import (
"fmt"
)

func main() {

var message string
message = "hello, world"

//message := "hello, world"

fmt.Println(message)
}

const 常量,同时可以按组定义 (一次定义多个)。小写定义的变量,包外不可见。
自动重复上面的规则上一个枚举定义的规则:

1
2
3
4
5
6
7
8
9
10
11
12
package main

import "fmt"

const (
answer1 = iota*2
answer2
)

func main() {
fmt.Println(answer1, answer2)
}

普通变量的省略写法:

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

func main() {
//nine := 9
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 main

import "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 main

import (
"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 main

import (
"os"
"fmt"
)

func main() {
n, err := fmt.Println("HelloWorld")

switch { //switch这里不写枚举,下面case进行条件判断
case err != nil:
os.Exit(1)
case n == 0:
fmt.Println("0 bytes output")
case n != 11: /*including \n*/
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
for {
//do sth.
}

带有判断的循环:

1
2
3
4
5
var counter int
counter = 0
for counter < 10 {
//do sth
}

普通循环:

1
2
3
for counter := 0; counter <10; counter++ {
//do sth
}

第一部分和第三部分可以同时赋值多个变量,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 main

import (
"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 main

import (
"fmt"
)

func main() {
//words := [...]string{"the", "world","is","ours."}
words := [4]string{"the", "world","is","ours."}
//words := []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 main

import (
"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) //初始元素个数 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 main

import (
"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 main

import (
"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 本身提示
  • 使用 errors 包

使用 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 main

import (
"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 fmt.Errorf("Unwilling to print an empty string")
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 main

import "fmt"

func emit(c chan string) {
words := []string {"the", "quick", "brown", "fox"}

for _, word := range words {
c <- word //循环写入 chan
}
close(c)
}

func main() {
wordChan := make(chan string) //不指定队列大小,默认不使用队列

go emit(wordChan)

for word := range wordChan {//事先并不知知道要对方要写几个
fmt.Printf("%s ", word)
}
//word := <- wordChan //这样只能拿到一个, 然后程序结束

fmt.Print("\n")
}

实际上从 chan 拿到内容的时候,可以检查一下是否关闭了:

1
word, isOpen := <- wordChan  //false表示已经关闭

如果发送完毕,不关闭 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 main

import "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) //收取对方写入的内容,10次
}
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 main

import "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) //收取对方写入的内容,10次
}
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 main

import (
"time"
"fmt"
)

func emit(wordChan chan string, done chan bool) {
words := []string{"the","quick","brown","fox"}

i := 0
t := time.NewTimer(3 * time.Second) //三秒后执行 timeout 分支

defer close(wordChan)

for {
select {
case wordChan <- words[i]: //写出去
i++
if i == len(words) {
i = 0 //再重头开始
}
case <-done: //收到停止的信息,停止接收
fmt.Println("Got done")
//close(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 main

import (
"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 main

import (
"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 main

import (
"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) //waiting for work
}

urlChan <- url //这个分发工作也可以专门开多个协程进行 func dispath(url string, urlChan chan string)
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 main

import (
"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: //3秒钟到了,暂停接收(ignore, 忽略)--chan接收一直走该分支
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 main

import (
"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) //5秒过,那些阻塞等待的goroutines可以继续执行
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:
//todo other things
}
}

这里等到消息可以是真的接收到或者,收到 close 消息。

总结一条,其实就是停止阻塞&&关闭 chan.

运行时Type

其实就是 interface{},编译时确定不了,只有运行时才知道具体的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import (
"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 main

import (
"fmt"
)

func whatisThis(i interface{}) {
//fmt.Printf("%T\n", i)
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)) //int 值判断为 string,必定失败,panic

接口

没有继承结构 && 类型体系,只有 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) {
//do sth.
}

type intSlice []int //声明一种类型

func (is intSlice) Len() int {
//do sth
}

func (is intSlice) Swap(i, j int) {
//do sth
}

接口表明了一种能力,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 main

import (
"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)

//开启 1000 个协程
for i:= 0; i < 1000; i++ {
go worker(sema)
}

for i:= 0; i< 10; i++ {
sema <-true //虽然开启了 1000 个协程,但实际工作的只有 10个
}

time.Sleep(10 * time.Second) //让子 routines 有足够的时间执行
}

其实这里控制了 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 main

import (
"time"
"fmt"
"math/rand"
"sync/atomic"
)

var (
runningCount int64 = 0
)

func worker(sema chan bool) {
//<- sema //队列为空则等待
atomic.AddInt64(&runningCount, 1) //消耗一个资源

//假装在做任务
fmt.Printf("[ %d", runningCount)
time.Sleep(time.Duration(rand.Intn(10)) * time.Second)
fmt.Print("]")

//完成任务了
//sema <-true
atomic.AddInt64(&runningCount, -1) //把资源还回来
}

func main() {

sema := make(chan bool, 10)

//开启 1000 个协程
for i:= 0; i < 1000; i++ {
go worker(sema)
}

for i:= 0; i< 10; i++ {
sema <-true //虽然开启了 1000 个协程,但实际工作的只有 10个
}

time.Sleep(10 * time.Second) //让子 routines 有足够的时间执行
}

可以看到用 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 main

import (
"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 main

import "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 Tools

为什么 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 相关的书不值得购买

文章目录
  1. 1. Hello
  2. 2. 目录讲解
  3. 3. 文档
  4. 4. 变量
  5. 5. 字符串
  6. 6. if语句
  7. 7. switch语句
  8. 8. for循环
  9. 9. 函数定义
  10. 10. 不定参数
  11. 11. 数组和切片
  12. 12. map
  13. 13. byte切片
  14. 14. 错误异常
  15. 15. 协程&chan
  16. 16. select语句
  17. 17. chan读写超时
  18. 18. 传递chan
  19. 19. 多个协程实现线程池
  20. 20. nilChan的应用
  21. 21. 关闭chan
  22. 22. 运行时Type
  23. 23. typeSwitch
  24. 24. 类型断言
  25. 25. 接口
  26. 26. 带缓冲的chan
  27. 27. 自定义类型
  28. 28. 多包引用
  29. 29. Go Tools
  30. 30. 单元测试
  31. 31. 标准库部分
  32. 32. Web开发
|