✨你好啊,我是“ 罗师傅”,是一名程序猿哦。
🌍主页链接:楚门的世界 - 一个热爱学习和运动的程序猿
☀️博文主更方向为:一起来学go(狗)语言。随着专业的深入会越来越广哦…一起期待。
❤️一个“不想让我曾没有做好的也成为你的遗憾”的博主。
💪很高兴与你相遇,一起加油!

前言

将对go语言的面向接口、函数式编程、错误处理和资源管理、测试、Goroutine、Channel进行初步学习。

❤️‍🔥❤️‍🔥❤️‍🔥面向接口

🍇🍇接口概念

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func getRetriever() retriever {
// 两个里边都实现了Get方法,但是没有实现接口,却能使用接口的功能
// return testing.Retriever{}
return infar.Retriever{}
}

// ?: Something that can "Get"
type retriever interface {
Get(url string) string
}

func main() {
var r retriever = getRetriever()
fmt.Println(r.Get("https://www.imooc.com"))
}
  • 强类型语言:熟悉接口的概念 “天上飞的理念,必然会有落地的实现”
  • 弱类型语言:没(少)有接口的概念

这里会存在一个疑问,为什么没有实现接口却能有接口的功能呢? 听哦细细道来

🍈🍈duck typing

大黄鸭是鸭子吗?(不同角度看待)

(这是因为duck typing是需要动态绑定的,但是go不具备)

  • 传统类型系统:脊索动物门,脊椎动物亚门,鸟纲雁形目。。。。。。

  • duck typing: 是鸭子

    • ”像鸭子走路,像鸭子叫(长得像鸭子),那么就是鸭子”
    • 描述事物的外部行为而非内部结构
    • 严格说go属于结构化类型系统,类似duck typing

🍉python中的duck typing

1
2
def download(retriever):
return retriever.get("www.imooc.com")
  • 运行时才知道传入的retriever有没有get
  • 需要注释来说明接口

🍊C++中的duck typing

1
2
3
4
template <class R>
string download(const R& retriever) {
return retriever.get("www.imooc.com")
}
  • 编译时才知道传入的retriever有没有get
  • 需要注释来说明接口

🍋java中的类似代码

1
2
3
4
<R extends Retriever>
String download(R r) {
return r.get("www.imooc.com")
}
  • 传入的参数必须实现Retriever接口
  • 不是duck typings

🍌go语言的duck typing

  • 同时需要Readable, Appendable怎么办?(apache polygene)
  • 同时具备python, c++的duck typing的灵活性
  • 又具有java的类型检查

🍍🍍接口的定义

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
// retriever实现者
type Retriever interface {
Get(url string) string
}
// download是使用者
func download(r Retriever) string {
return r.Get("http://www.imooc.com")
}
func main() {
var r Retriever
r = mock.Retriever{"this is a fake imooc.com"}
r = real2.Retriever{}
fmt.Println(download(r))
}
// 实现者1 mockRetriever
type Retriever struct {
Contents string
}
func (r Retriever) Get(url string) string {
return r.Contents
}
// 实现者2 realRetriever
type Retriever struct {
Contents string
}
func (r Retriever) Get(url string) string {
return r.Contents
}

注意点: interface中的Get(url string) string 必须 要和实现者中的Get方法参数类型一致

  • 接口的实现是隐式
  • 只要实现接口里的方法

🥭🥭接口的值类型

1
2
3
4
5
6
7
8
// 接口变量内 (值的类型, 值/指针)
fmt.Printf("%T %v\n", r, r)

// mock.Retriever {this is a fake imooc.com}
// Contents: this is a fake imooc.com

// *real.Retriever &{Mozilla/5.0 1m0s}
// UserAgent: Mozilla/5.0
  • 接口变量自带指针
  • 接口变量同样采用值传递,几乎不需要使用接口的指针
  • 指针接收者实现只能以指针方式使用;值接收者都可
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
// 值接收者
func (r Retriever) Get(url string) string {
return r.Contents
}
r = mock.Retriever{"this is a fake imooc.com"}
r = &mock.Retriever{"this is a fake imooc.com"} // 两种都行
inspect(r)

// 指针接收者
func (r *Retriever) Get(url string) string {
resp, err := http.Get(url)
if err != nil {
panic(err)
}
result, err := httputil.DumpResponse(resp, true)
defer resp.Body.Close()
if err != nil {
panic(err)
}
return string(result)
}
r = &real2.Retriever{ // 只能以指针的方式使用
UserAgent: "Mozilla/5.0",
TimeOut: time.Minute,
}

🍎查看接口变量

  • 表示任何类型:interface{}
  • Type Assertion
  • Type Switch
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
// interface{} 可以接收任何类型
type Queue []interface{}
func (q *Queue) Push(v interface{}) {
*q = append(*q, v.(int)) // 表示强制接收int类型
}
func (q *Queue) Pop() interface{} {
head := (*q)[0]
*q = (*q)[1:]
return head.(int) // 表示强制返回int类型
}

// Type assertion
if mockRetriever, ok := r.(mock.Retriever); ok {
fmt.Println(mockRetriever.Contents)
} else {
fmt.Println("not a mock retriever")
}

// Type Switch
switch v := r.(type) {
case mock.Retriever:
fmt.Println("Contents:", v.Contents)
case *real2.Retriever:
fmt.Println("UserAgent:", v.UserAgent)
}

🍏🍏接口的组合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type Retriever interface {
Get(url string) string
}
type Poster interface {
Post(url string, form map[string]string) string
}
// 接口的组合
type RetrieverPoster interface {
Retriever
Poster
}
// 源码中存在大量这样的设计
// ReadWriteCloser is the interface that groups the basic Read, Write and Close methods.
type ReadWriteCloser interface {
Reader
Writer
Closer
}

🍐🍐常用系统接口

  • Stringer (相当于java的toSring)
  • Reader/Writer (有了更强大的读写功能,不单单只是读写文件)
1
2
3
4
5
// 相当于是重写了toString方法
func (r *Retriever) String() string {
//TODO implement me
return fmt.Sprintf("Retriever: {Contents=%s}", r.Contents)
}
1
2
3
4
5
6
7
// 使用io.Reader功能更强大了
func printFileContents(reader io.Reader) {
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
}

🍑🍑🍑函数式编程

🍒🍒函数式编程 vs 函数指针

  • 函数式一等公民:参数,变量,返回值都可以是函数
  • 高阶函数
  • 函数 -> 闭包

🍓🍓”正统“ 函数式编程

  • 不可变性:不能有状态,只能有常量和函数
  • 函数只能有一个参数
1
2
3
4
5
6
7
// 正统的函数式编程满足了上面的两个条件
type iAdder func(int) (int, iAdder)
func adder2(base int) iAdder {
return func(v int) (int, iAdder) {
return base + v, adder2(base + v)
}
}

🫐🫐闭包

1
2
3
4
5
6
7
8
// 闭包
func adder() func(int) int {
sum := 0 // 自由变量
return func(v int) int { // 返回的是函数的引用,同时会把自由变量保存下来
sum += v
return sum
}
}

🥝python中的闭包

1
2
3
4
5
6
7
def adder():
sum = 0
def f(value):
nonlocal sum
sum += value
return sum
return f
  • python原生支持闭包
  • 使用__closure__来查看闭包内容

🍅C++中的闭包

1
2
3
4
5
6
7
8
// 要在c++14才能跑通这段代码
auto adder() {
auto sum = 0;
return [=] (int value) mutable {
sum += value;
return sum;
}
}
  • 过去:stl或者boost带有类似库
  • c++11及以后:支持闭包

🫒java中的闭包

1
2
3
4
5
6
7
8
9
10
11
// Holder就是自定义的一个class
class Holder() {
Integer value;
}
Function<Integer, Integer> adder() {
final Holder<Integer> sum = new Holder<>(0);
return (Integer value) -> {
sum.value += value;
return sum.value;
}
}
  • 1.8以后:使用Function接口和Lambda表达式来创建函数对象
  • 匿名类或Lambda表达式均支持闭包

🥥go语言闭包的使用

  • 斐波那契数列
1
2
3
4
5
6
7
8
9
10
// 1, 1, 2, 3, 5, 8, 13, 21, 34, 55
// a, b
// a, a+b
func fibonacci() func() int {
a, b := 0, 1
return func() int {
a, b = b, a+b
return a
}
}
  • 为函数实现接口
1
2
3
4
5
6
7
8
9
type intGen func() int
func (g intGen) Read(p []byte) (n int, err error) {
next := g()
if next > 10000 {
return 0, io.EOF
}
s := fmt.Sprintf("%d\n", next)
return strings.NewReader(s).Read(p)
}
  • 使用函数来遍历二叉树
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func (node *Node) TraverseFunc(f func(*Node)) {
if node == nil {
return
}
node.Left.TraverseFunc(f)
f(node) // 由自己决定该做什么
node.Right.TraverseFunc(f)
}
// 统计结点个数
nodeCount := 0
root.TraverseFunc(func(node *tree.Node) {
nodeCount++
})
fmt.Println("Node count: ", nodeCount)

工语言闭包的应用

  • 更为自然,不需要修饰如何访问自由变量
  • 没有Lambda表达式,但是又匿名函数

🥑🥑🥑错误处理和资源管理

🍆🍆defer调用

  • 确保调用在函数结束时发生
1
2
3
4
5
6
7
func tryDefer() { // defer 会在函数结束时调用 栈的方式
defer println(1)
defer println(2)
println(3)
panic("error occurred")
println(4)
}
  • 参数在defer语句时计算
1
2
3
4
5
6
7
8
func tyrDefer2() {
for i := 0; i < 100; i++ {
defer fmt.Println(i)
if i == 30 {
panic("printed too many")
}
}
}
  • defer列表为后进先出(栈)

🥔何时使用defer调用

  • Open/Close
  • Lock/Unlock
  • PrintHeader/PrintFooter

🥕🥕错误处理一

1
2
3
4
5
6
7
8
file, err := os.Open("abc.txt")
if err != nil {
if pathError, ok := err.(*os.PathError); ok {
fmt.Println(pathError.Err)
} else {
fmt.Println("unknown error", err)
}
}

🌽🌽错误处理二

  • 如何实现统一的错误处理逻辑(封装+函数作为参数和返回值)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// handler.go
// 封装操作和产生错误信息的过程
func HandleFileList(writer http.ResponseWriter, request *http.Request) error {
path := request.URL.Path[len("/list/"):]
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
bytes, err := ioutil.ReadAll(file)
if err != nil {
return err
}
writer.Write(bytes)
return nil
}
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
// web.go
// 函数作为参数和返回值
type appHandler func(writer http.ResponseWriter, request *http.Request) error

// 用于统一处理错误
func errWrapper(handler appHandler) func(http.ResponseWriter, *http.Request) {
return func(writer http.ResponseWriter, request *http.Request) {
err := handler(writer, request)
//defer func() {
// r := recover()
// if r != nil {
// log.Printf("Panic: %v", r)
// http.Error(writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
// }
//}()
if err != nil {
log.Println("Error handling request: ", err.Error())
//if userErr, ok := err.(userError); ok { // 用户可以看到的错误
// http.Error(writer, userErr.Message(), http.StatusBadRequest)
// return
//}
code := http.StatusOK
switch {
case os.IsNotExist(err):
code = http.StatusNotFound // 404 未找到
case os.IsPermission(err):
code = http.StatusForbidden // 403 禁止访问
default:
code = http.StatusInternalServerError // 500 内部服务器错误
}
http.Error(writer, http.StatusText(code), code)
}
}
}
// main函数入口
func main() {
http.HandleFunc("/list/", errWrapper(filelisting.HandleFileList))
// http://localhost:8888/list/fib.txt
err := http.ListenAndServe(":8888", nil)
if err != nil {
panic(err)
}
}

现在要确定哪些错误是可以给用户看到的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// handler.go 增加了
const prefix = "/list/"
type userError string
func (u userError) Error() string { // 给开发者看的错误信息
//TODO implement me
return u.Message()
}
func (u userError) Message() string { // 给用户看的错误信息
//TODO implement me
return string(u)
}
func HandleFileList(writer http.ResponseWriter, request *http.Request) error {
// 判断路径是否是以 /list/ 开头
if strings.Index(request.URL.Path, prefix) != 0 {
return userError("path must start with " + prefix)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// web.go 增加了
type userError interface {
error // 给开发者看的错误信息
Message() string // 给用户看的错误信息
}
// errWapper中增加部分
// 自定义recover后的处理
defer func() {
r := recover()
if r != nil {
log.Printf("Panic: %v", r)
http.Error(writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
}()
// 判断是不是用户错误
if userErr, ok := err.(userError); ok {
http.Error(writer, userErr.Message(), http.StatusBadRequest)
return
}

🌶🌶recover

recover就是在确保程序出现error或者panic后不宕机,这是go语言对应服务错误的一种保护机制

  • 仅在defer调用中使用
  • 获取panic的值
  • 如果无法处理,可重新panic

🫑🫑error vs panic

  • 意料之中的:使用error。如:文件打不开
  • 意料之外的:使用panic。如:数组越界

🥒🥒错误处理的综合实例

  • defer + panic + recover
  • Type Assertion
  • 函数式编程的应用

对Type Assertion进行解释:

go提供了一种方式让我们可以判断某个接口所绑定的类型是否携有某种其他类型

1
2
t, ok := i.(T)
userErr, ok := err.(userError) //判断返回的err中有没有userError这种类型

🦀🦀🦀测试

🦞🦞传统测试 vs 表格驱动测试

🦐传统测试

1
2
3
4
5
6
7
@Test public void testAdd() { // java
assertEquals(3,add(1,2));
assertEquals(2,add(0,2));
assertEquals(0,add(0,0));
assertEquals(0,add(-1,1));
assertEquals(Integer.MIN_VALUE, add(1, Integer.MAX_VALUE));
}
  • 测试数据和测试逻辑混在一起
  • 出错信息不明确
  • 一旦一个数据出错测试全部结束

🦑表格驱动测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 测试勾股定理
func TestTriangle(t *testing.T) {
tests := []struct{ a, b, c int }{
{3, 4, 5},
{5, 12, 13},
{8, 15, 17},
{12, 35, 37},
{30000, 40000, 50000},
}
for _, tt := range tests {
if actual := calcTriangle(tt.a, tt.b); actual != tt.c {
t.Errorf("calcTriangle(%d,%d); "+
"got %d; expected %d",
tt.a, tt.b, actual, tt.c)
}
}
}
  • 分离的测试数据和测试逻辑
  • 明确的出错信息
  • 可以部分失败

🦪🦪代码覆盖率

下面使用命令行cmd进行测试

  • go test -coverprofile="c.out"注意:我是要加上“”才能执行成功)
  • go tool cover -html="c.out"

使用命令行cmd进行测试

🍦🍦性能测试

1
2
3
4
5
6
7
8
9
10
11
12
// 测试速度
func BenchmarkSubstr(b *testing.B) {
s := "黑化肥挥发发灰会花飞灰化肥挥发发黑会飞花"
ans := 8

for i := 0; i < b.N; i++ {
actual := lengthOfNonRepeatingSubStr(s)
if actual != ans {
b.Errorf("Got %d for input %s; expected %d", actual, s, ans)
}
}
}
1
2
3
4
5
6
7
goos: windows 
goarch: amd64
pkg: imooc.com/ccmouse/learngo/chapter01/nonrepeatingsubstr
cpu: Intel(R) Core(TM) i5-1035G1 CPU @ 1.00GHz
BenchmarkSubstr
BenchmarkSubstr-8 1224705(测试次数) 1014 ns/op(每次耗费的时间)
PASS
  • go test -bench .

🍧🍧使用pprof进行性能调优

步骤如下:

  • go test -bench . (选做)
  • go test -bench . -cpuprofile cpu.out
  • go tool pprof .\cpu.out
  • (pprof) web (前提是下载好Graphviz,同时配置好环境变量即可)

选择 Add Graphiz to the system PATH for all users 后自动配置环境变量

红框越大,箭头越粗则表示耗时越长

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 根据Graphviz图选择对map进行优化 空间换时间
var lastOccurred = make([]int, 0xffff) // 65535
func lengthOfNonRepeatingSubStr(s string) int {
//lastOccurred := make(map[rune]int) map耗时多
// lastOccurred['e'] = 1
// lastOccurred[0x65] = 6 'e' = 0x65
for i := range lastOccurred {
lastOccurred[i] = -1
}
start := 0
maxLength := 0
for i, ch := range []rune(s) {
if lastI := lastOccurred[ch]; lastI != -1 && lastI >= start {
start = lastI + 1
}
if i-start+1 > maxLength {
maxLength = i - start + 1
}
lastOccurred[ch] = i
}
return maxLength
}

性能调优步骤:

  • -cpuprofile获取性能数据
  • go tool pprof 查看性能数据
  • 分析慢在哪里
  • 优化代码

🍨🍨http测试

  • 通过使用假的Request/Response (快,粒度细,像单元测试)
  • 通过起服务器 (集成度高,代码覆盖率高,但是慢)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 只是测试了错误处理函数
func TestErrorWrapper(t *testing.T) {
for _, tt := range tests {
f := errWrapper(tt.h)
response := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "http://www.imooc.com", nil)
f(response, request)
verifyResponse(response.Result(), tt.code, tt.message, t)
}
}
// 测试了整个服务器
func TestErrWrapperInServer(t *testing.T) {
for _, tt := range tests {
f := errWrapper(tt.h)
server := httptest.NewServer(http.HandlerFunc(f))
resp, _ := http.Get(server.URL)
verifyResponse(resp, tt.code, tt.message, t)
}
}

🍩🍩生成文档和实例代码

使用 go doc 查看文档

使用 godoc 生成文档

注意可能会执行失败,说无法识别godoc

  • go get golang.org/x/tools/cmd/godoc 执行该指令即可(最好科学上网)

成功后执行 godoc -http :6060

可以看到自己项目的目录结构了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 对应注释 queue.go
// An FIFO queue
type Queue []interface{}
// Pushes the element into the queue
// q.Push(123)
func (q *Queue) Push(v interface{}) {
*q = append(*q, v)
}
// Pops element from head
func (q *Queue) Pop() interface{} {
head := (*q)[0]
*q = (*q)[1:]
return head.(int)
}
// Returns if the queue is empty or not
func (q *Queue) IsEmpty() bool {
return len(*q) == 0
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 对应代码 queue_test.go
func ExampleQueue_Pop() {
q := Queue{1}
q.Push(2)
q.Push(3)
fmt.Println(q.Pop())
fmt.Println(q.Pop())
fmt.Println(q.IsEmpty())
fmt.Println(q.Pop())
fmt.Println(q.IsEmpty())

// Output: 要写了Output才会有执行箭头
// 1
// 2
// false
// 3
// true
}

🍪文档

  • 用注释写文档
  • 在测试中加入Example
  • 使用 go doc (查看) / godoc(生成)文档

🎂🎂🎂Goroutine

只需要一个go关键字就可以实现并发编程啦

1
2
3
4
5
6
7
8
9
for i := 0; i < 1000; i++ {
go func(i int) {
for {
// I/O操作会进行线程(协程)切换
fmt.Printf("Hello from goroutine %d\n", i)
}
}(i)
}
time.Sleep(time.Millisecond)
1
2
3
4
5
6
7
8
9
10
11
12
// 去掉I/O操作,验证是否是非抢占式的协程
var a [10]int
for i := 0; i < 10; i++ {
go func(i int) {
for {
a[i]++
runtime.Gosched() // 交出控制权
}
}(i)
}
time.Sleep(time.Millisecond)
fmt.Println(a)
  • go run -race .\goroutine.go 可以查看是否存在 race condition! (计算机)竞态条件

🍬🍬协程 Coroutine

go 内部是 协程

  • 轻量级“线程”
  • 非抢占式多任务处理,由协程主动交出控制权
  • 编译器/解释器/虚拟机层面的多任务(os没有协程)
  • 多个协程可能在一个或多个线程上运行

🍭🍭go语言的调度器

🐕其他语言中的协程

  • C++:Boost.Coroutine
  • Java:不支持
  • python中的协程
    • 使用yield关键字实现协程
    • Python3.5加入了async def对协程原生支持

🥵🥵goroutine的定义

  • 任何函数只需加上go就能送给调度器运行
  • 不需要在定义时区分是否是异步函数
  • 调度器在合适的点进行切换
  • 使用-race来检测数据访问冲突

🤓🤓goroutine可能的切换点

  • I/O,select
  • channel
  • 等待锁
  • 函数调用(有时)
  • runtime.Gosched()

只是参考,不能保证切换,不能保证在其他地方不切换

🤲🤲查看线程数

Linunx 使用 top, Windows没有找到💦💦💦

这里的 X/Y: Y的最大值取决于电脑的核数

🦊🦊🦊Channel(管道)

Channel的理论基础:Communication Sequential Process(CSP)

  • Channel as first-class citizen
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// channel作为一等公民,可以用作参数,返回值,数组
func createWorker(id int) chan<- int {
c := make(chan int)
go func() {
for {
fmt.Printf("Worker %d received %c\n", id, <-c)
}
}()
return c
}
func chanDemo() {
var channels [10]chan<- int
for i := 0; i < 10; i++ {
channels[i] = createWorker(i)
}
for i := 0; i < 10; i++ {
channels[i] <- 'a' + i
}
for i := 0; i < 10; i++ {
channels[i] <- 'A' + i
}
time.Sleep(time.Millisecond)
}

这里有个难点就是如何区分 chan<- 和 <-chan

我的理解是:这里的 n = 变量

  • chan<- 表明数据只能向管道发送,(对于n,即只能发送不能接收)

  • <-chan 表明数据只能从管道出去,(对于n,即只能接收不能发送)

  • Buffered channel

一般只要channel中有数据但是没有人接收,那么就会报错 !deadLock!

1
2
3
4
5
6
7
8
9
func bufferedChannel() {
c := make(chan int, 3) // 表示可以缓存3个数据
go worker(0, c)
c <- 'a'
c <- 'b'
c <- 'c'
c <- 'd'
time.Sleep(time.Millisecond)
}
  • Channel close and range
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 两种发送方关闭方式
// 方式一
for n := range c {
fmt.Printf("Worker %d received %c\n", id, n)
}
// 方式二
for {
n, ok := <-c
if !ok {
break
}
fmt.Printf("Worker %d received %c\n", id, n)
}
// 发送方主动关闭
func channelClose() {
c := make(chan int, 3)
go worker(0, c)
c <- 'a'
c <- 'b'
c <- 'c'
c <- 'd'
close(c)
time.Sleep(time.Millisecond)
}

🐵🐵例一:使用Channel来等待goroutine结束

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
>// done.go
type worker struct {
in chan int
done func()
}
func doWork(id int, w worker) {
for n := range w.in {
fmt.Printf("Worker %d received %c\n", id, n)
//go func() { done <- true }() 最直接的一种解决方法
w.done()
}
}
func createWorker(id int, wg *sync.WaitGroup) worker {
w := worker{
in: make(chan int),
done: func() {
wg.Done()
},
}
go doWork(id, w)
return w
}
func chanDemo() {
var wg sync.WaitGroup
var workers [10]worker
for i := 0; i < 10; i++ {
workers[i] = createWorker(i, &wg)
}
wg.Add(20)
for i, worker := range workers {
worker.in <- 'a' + i
}
for i, worker := range workers {
worker.in <- 'A' + i
}
wg.Wait()
// wait for all of them
}
  • WaitGroup的使用
  • 声明 var wg sync.WaitGroup
  • wg.Add(n)“添加”会将增量(可能为负数)添加到等待组计数器
  • wg.Done 本质就是 wg.Add(-1)
  • wg.Wait() 等待块,直到等待组计数器为零。
  • 如果计数器变为零,则释放在 Wait 上阻塞的所有 goroutine。

🐒🐒例二:使用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
// TraverseFunc 中序遍历
func (node *Node) TraverseFunc(f func(*Node)) {
if node == nil {
return
}
node.Left.TraverseFunc(f)
f(node)
node.Right.TraverseFunc(f)
}
func (node *Node) TraverseWithChannel() chan *Node {
out := make(chan *Node)
go func() {
node.TraverseFunc(func(node *Node) {
out <- node
})
defer close(out)
}()
return out
}

// 使用 channel 遍历
c := root.TraverseWithChannel()
maxNode := 0
for node := range c {
if node.Value > maxNode {
maxNode = node.Value
}
}
fmt.Println("Max node value: ", maxNode)

🦍🦍例三:使用Select来进行调度

  • Select 的使用
  • 定时器的使用
  • 在Select中使用Nil Channel

Nil Channel进行解释

当未为channel分配内存时,channel就是nil channel,例如var ch1 chan int。nil channel会永远阻塞对该channel的读、写操作。

nil channel会阻塞对该channel的所有读、写。所以,可以将某个channel设置为nil,进行强制阻塞,对于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
// generator只是生产数据
func generator() chan int {
out := make(chan int)
go func() {
i := 0
for {
time.Sleep(time.Duration(rand.Intn(1500)) * time.Millisecond)
out <- i
i++
}
}()
return out
}
// 真正做事的是worker
func worker(id int, c chan int) {
for n := range c {
time.Sleep(time.Second)
fmt.Printf("Worker %d received %d\n", id, n)
}
}
func createWorker(id int) chan<- int {
c := make(chan int)
go worker(id, c)
return c
}
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
func main() {
var c1, c2 = generator(), generator()
var worker = createWorker(0)
var values []int
tm := time.After(10 * time.Second)
tick := time.Tick(time.Second)
for {
var activeWorker chan<- int
var activeValue int
if len(values) > 0 {
activeWorker = worker
activeValue = values[0]
}
select {
case n := <-c1:
values = append(values, n)
case n := <-c2:
values = append(values, n)
case activeWorker <- activeValue: // 这里使用了Nil Channel机制
values = values[1:]
case <-time.After(800 * time.Millisecond):
fmt.Println("timeout")
case <-tick:
fmt.Println("queue len = ", len(values))
case <-tm:
fmt.Println("bye")
return
}
}
}

🦧🦧传统同步机制

  • WaitGoup
  • sync.Mutex 通常用来保护临界区和共享资源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type atomicInt struct {
value int
lock sync.Mutex
}
func (a *atomicInt) increment() {
fmt.Println("safe increment")
func() {
a.lock.Lock()
defer a.lock.Unlock()
a.value++
}()
}
func (a *atomicInt) get() int {
a.lock.Lock()
defer a.lock.Unlock()
return a.value
}
  • sync.Cond 用来协调想要访问的共享资源

🐶🐶并发编程模式

  • 生成器
1
2
3
4
5
6
7
8
9
10
11
12
13
// 消息生成器 返回chan
func msgGen(name string) chan string {
c := make(chan string)
go func() {
i := 0
for {
time.Sleep(time.Duration(rand.Intn(2000)) * time.Millisecond)
c <- fmt.Sprintf("service: %s, message: %d", name, i)
i++
}
}()
return c
}
  • 服务/任务
1
2
3
4
// 相当于拿到了不同的服务
m1 := msgGen("service1")
m2 := msgGen("service2")
// m := fanIn(m1, m2)
  • 同时等待多个服务:两种方法

方法一:开多个goroutine

1
2
3
4
5
6
7
8
9
10
11
12
// 适用于我们不知道有多少个channel的情况
func fanIn(chs ...chan string) chan string {
c := make(chan string)
for _, ch := range chs {
go func(ch chan string) { // 使用函数传参避免问题
for {
c <- <-ch
}
}(ch)
}
return c
}

方法二:Select

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 适用于我们知道有多少个channel的情况
func fanInBySelect(c1, c2 chan string) chan string {
c := make(chan string)
go func() {
for {
select {
case m := <-c1:
c <- m
case m := <-c2:
c <- m
}
}
}()
return c
}

🦝🦝任务的控制

  • 非阻塞等待
    • 如果m1到了m2没有到,就会 no message received
    • 如果m1到了同时m2也到了,就会输出 service service2:message ?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func nonBlockingWait(c chan string) (string, bool) {
select {
case m := <-c:
return m, true
default:
return "", false
}
}
// main
m1 := msgGen("service1")
m2 := msgGen("service2")
for {
fmt.Println(<-m1)
if m, ok := nonBlockingWait(m2); ok {
fmt.Println(m)
} else {
fmt.Println("no message received")
}
}
  • 超时机制
    • 如果 1000*time.Millisecond内到了,输出:service service1:message ?
    • 如果在指定时间内没有到,输出:timeout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func timeoutWait(c chan string, timeout time.Duration) (string, bool) {
select {
case m := <-c:
return m, true
case <-time.After(timeout):
return "", false
}
}
// main
m1 := msgGen("service1")
for i := 0; i < 5; i++ {
if m, ok := timeoutWait(m1, 1000*time.Millisecond); ok {
fmt.Println(m)
} else {
fmt.Println("timeout")
}
}
  • 任务中断/退出
  • 优雅退出
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
// 上面两点的结合
func msgGen(name string, done chan struct{}) chan string {
c := make(chan string)
go func() {
i := 0
for {
select {
case <-time.After(time.Duration(rand.Intn(2000)) * time.Millisecond):
c <- fmt.Sprintf("service: %s, message: %d", name, i)
case <-done: // 外面通知我要退出了
fmt.Println("Cleaning Up")
time.Sleep(2 * time.Second)
fmt.Println("Cleaning Done")
done <- struct{}{} // 通知外面我已经退出了
return
}
i++
}
}()
return c
}
// main
// 第一个{}表示struct{}类型的值,第二个{}表示初始化该类型
done <- struct{}{} // 通知service1退出
<-done // 等待service1退出

🐱🐱🐱迷宫的广度优先搜索

🦌🦌广度优先算法(BFS)

  • 为爬虫实战项目做好准备
  • 应用广泛,综合性强
  • 面试常见

广度优先搜索走迷宫:

  • 用循环创建二维slice
1
2
3
4
5
// 初始化步数矩阵 全都为0
steps := make([][]int, len(maze))
for i := range steps {
steps[i] = make([]int, len(maze[i]))
}
  • 使用slice来实现队列
1
2
3
4
Q := []point{start} // 初始化队列
cur := Q[0]
Q = Q[1:]
Q = append(Q, next)
  • scanner := bufio.NewScanner(file)读取文件
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
func readMaze(filename string) [][]int {
file, err := os.Open(filename)
if err != nil {
panic(err)
}
scanner := bufio.NewScanner(file)

// 读取迷宫的行数和列数
scanner.Scan()
dimensions := strings.Split(scanner.Text(), " ") // Split-->[]string
row, _ := strconv.Atoi(dimensions[0]) // string-->int
col, _ := strconv.Atoi(dimensions[1])

// 创建迷宫
maze := make([][]int, row)
for i := range maze {
maze[i] = make([]int, col)
}

// 逐行读取迷宫
for i := 0; i < row && scanner.Scan(); i++ {
line := strings.Split(scanner.Text(), " ")
for j := 0; j < col; j++ {
mazeValue, _ := strconv.Atoi(line[j])
maze[i][j] = mazeValue
}
}

if err := scanner.Err(); err != nil {
fmt.Println("读取迷宫文件失败:", err)
}
return maze
}
  • 对Point的抽象
1
2
3
func (p point) add(r point) point {
return point{p.i + r.i, p.j + r.j}
}

🐗🐗🐗http及其他标准库

🐭🐭http

  • 使用http客户端发送请求
1
2
3
4
request, err := http.NewRequest(http.MethodGet, "http://www.imooc.com", nil)
if err != nil {
panic(err)
}
  • 使用http.Client控制请求头部等
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 添加请求头信息
request.Header.Add("User-Agent", "Mozilla/5.0 (Linux; Android 10; Redmi K30 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Mobile Safari/537.36")
// 查看是否发生重定向
client := http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
fmt.Println("Redirect:", req)
return nil
},
}
resp, err := client.Do(request)
if err != nil {
panic(err)
}
defer resp.Body.Close()
  • httputil简化工作
1
2
3
4
5
// 读取响应内容
s, err := httputil.DumpResponse(resp, true)
if err != nil {
panic(err)
}

🐇http服务器的性能分析

  • import _ “net/http/pprof”
  • 访问http://ip:port/debug/pprof
  • 使用 go tool pprof分析性能

🐿🐿JSON的解析

  • JSON数据格式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"id": "1234",
"item": [
{
"id": "item_1",
"name": "learn go",
"price": 15
},
{
"id": "item_2",
"name": "learn java",
"price": 20
}
],
"quantity": 1,
"total_price": 20
}
  • 结构体的tag
1
2
3
4
5
6
7
8
9
10
11
12
type OrderItem struct {
ID string `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
}

type Order struct { // tag
ID string `json:"id"`
Items []OrderItem `json:"item"`
Quantity int `json:"quantity"`
TotalPrice float64 `json:"total_price"`
}
  • JSON Marshal与Unmarshal,数据类型
1
2
3
4
// Marshal
b, err := json.Marshal(o)
// Unmarshal
err := json.Unmarshal([]byte(s), &o)
  • 第三方API的解析技巧(选择struct形式)
1
2
3
4
5
6
7
8
9
10
11
12
// map形式接收数据
m := make(map[string]interface{})
fmt.Printf("%+v\n", m["data"].([]interface{})[2].(map[string]interface{})["synonym"].(string))

// struct形式接收数据
m := struct {
Data []struct {
Synonym string `json:"synonym"`
Tag string `json:"tag"`
} `json:"data"`
}{}
fmt.Printf("%+v, %+v\n", m.Data[0].Synonym, m.Data[0].Tag)

🦫🦫gin框架的介绍

  • gin-gonic/gin (依赖)建议将go版本换成 go1.20.5
    • go get -u github.com/gin-gonic/gin
    • get -u go.uber.org/zap
  • middleware的使用:功能感觉像java的AOP
  • Context的使用
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
func main() {
r := gin.Default()
logger, err := zap.NewProduction()
if err != nil {
panic(err)
}

// middleware 中间件
const keyRequestId = "requestId" // Context
r.Use(func(c *gin.Context) {
s := time.Now()
c.Next()
// 自定义日志
// path, satatus, latency
logger.Info("incoming request",
zap.String("path", c.Request.URL.Path),
zap.Int("status", c.Writer.Status()),
zap.Duration("latency", time.Now().Sub(s)))
}, func(c *gin.Context) { // Context的使用
c.Set(keyRequestId, rand.Int())
c.Next()
})

r.GET("/ping", func(c *gin.Context) {
h := gin.H{
"message": "pong gin",
}
if rid, exists := c.Get(keyRequestId); exists {
h[keyRequestId] = rid.(int) // 添加keyRequestId字段
}
c.JSON(200, h)
})
r.Run()
}

❤️❤️❤️忙碌的敲代码也不要忘了浪漫鸭!

此番周旋,劳顿滔天,压力堆积如山,只能于夜幕降临之际寻觅一丝空闲,进行种种总结。直至此刻才告一段落,触及一种轻松自在之感,这便是所谓苦尽甘来的真谛。💪