第1章 Go语言简单介绍

深度理解Go语言面向接口,函数式编程,错误处理,测试,并行计算等元素

Go语言的设计初衷

1.针对其他语言的痛点进行设计;2.并加入并发编程;3.为大数据、微服务、并发而生的通用编程语言。

Go语言与转型

项目转型首选语言;软件工程师转型、添加技术栈的首选语言;这是一门为转型量身定制的课程。

Go语言很特别:1.没有“对象”,没有继承多态,没有泛型,没有try/catch。2.有接口,函数式编程,CSP并发模型(goroutine+channel)。

示例 语法部分:每个小片段解决实际问题

综合部分:经典算法+典型例题+微型项目

实战项目部分:搭建分布式爬虫,结构复杂

基本语法:变量;选择,循环;指针,数组,容器

面向接口编程:结构体;duck typing的概念;组合的思想

函数式编程:闭包的概念;多样的例题

工程化:资源管理,错误处理;测试和文档;性能调优

并发编程:gorountine和channel;理解调度器;多样的例题

实战项目:从0开始,使用Go语言自主搭建简单分布式爬虫。爬取相亲网站资料;

Go语言的安装与开发环境

下载Go安装包:https://studygolang.com/dl

开发环境:idea + go插件

第2章 基本语法

变量,常量,类型,选择,循环,函数,指针

2-1 变量定义

使用var关键字

var a, b, c bool

var s1, s2 string = "hello, "world"

可放在函数内,或直接放在包内

使用var()集中定义变量

让编译器自动决定类型

var a, b, i, s1, s2 = true,false, 3, "hello", "world"

使用:=定义变量,只能在函数内使用

a, b, i, s1, s2 := true, false, 3, "hello', "world"

package mainimport "fmt"
//函数外定义变量,不是全局变量,是包内变量
var (aa = 3ss = "kkk"bb = true
)//函数里定义变量
func variableZerovalue() {var a intvar s stringfmt.Printf("%d %q\n", a,s)
}//变量赋初值
func variableInitialvalue() {var a, b int = 3, 4var s string = "abc"fmt.Println(a, b, s)
}func variableTypeDeduction() {var a, b, c, s = 3, 4, true, "def"fmt.Println(a, b, c, s)
}func variableShorter() {a, b, c, s := 3, 4, true, "def"b = 5fmt.Println(a, b, c, s)
}func main() {fmt.Println("Hello,World!")variableZerovalue()variableInitialvalue()variableShorter()fmt.Println(aa, ss, bb)
}

内建变量类型

bool, string

(u)int, (u)int8,(u)int16,(u)int32,(u)int64,uintptr

byte字节类型,rune字符类型32位

float32, float64,complex64实部虚部32位,complex128实部虚部64位

package mainimport fmtfunc euler() {//c := 3 + 4i//fmt.Println(complx.Abs(c))//fmt.Println(//    cmplx.Pow(math.E, 1i * math.Pi) + 1)fmt.Printf("%.3f\n",cmplx.Exp(1i * math.Pi) + 1)
}func main() {euler()
}

强制类型转换

类型转换是强制的

var a, b int = 3, 4

package mainimport fmtfunc triangle() {var a, b int 3, invar c intc = int(math.Sqrt(float64(a * a + b * b)))fmt.Println(c)
}func main() {triangle()
}

2-3 常量与枚举

常量的定义

const filename = “abc.txt”

const数值可作为各种类型使用
const a, b = 4
var c int = int(math.Sqrt(a*a + b*b))

使用常量定义枚举类型

普通枚举类型

自增值枚举类型

package mainimport ("fmt""math"
)
//常量定义包内,所有函数都能用。
//const filename = "abc.txt"
func consts() {const (filename = "abc.txt"a, b = 3, 4)var c intc = int(math.Sqrt(a*a + b*b))fmt.Println(filename, c)
}func enums() {const (cpp = iota_pythongolangjavascrip)//b, kb, mb, gb, tb, pbconst (//iota作为自增值的种子b = 1 << (10 * iota)kbmbgbtbpb)fmt.Println(cpp, javascrip, python, golang)fmt.Println(b, kb, mb, gb, tb, pb)
}
func main(){consts()enums()
}

变量定义要点回顾

变量类型写在变量名之后

编译器可推测变量类型

没有char,只有rune、

原生支持复数类型

2-4 条件语句

if

if的条件里不需要括号

func bounded(v int) int {if v > 100 {return 100} else if v < 0 {return 0} else {return v}
}

if的条件里可以赋值

if的条件里赋值的变量作用域就在这个if语句里

if contents, err := ioutil.ReadFile(filename); err != nil {
fmt.Println(err)
} else {
fmt.Printf("%s\n", contents)
}
package mainimport ("io/ioutil""fmt"
)func main() {const filename = "abc.txt"if contents, err := ioutil.ReadFile(filename); err != nil {fmt.Println(err)} else {fmt.Printf("%s\n", contents)}
/* if err != nil {fmt.Println(err)} else {fmt.Printf("%s\n", contents)}*/
}

switch
switch会自动break,除非使用fallthrough

switch后可以没有表达式

func eval(a, b int, op string) int {var result intswitch op {case "+":result = a + bcase "-":result = a - bcase "*":result = a * bcase "/":result = a / bdefault://panic报错panic("unsupported operator:" + op)}return result
}
package mainimport ("io/ioutil""fmt"
)func grade(score int) string {g := ""switch {case score < 0 || score > 100:panic(fmt.Sprintf("Wrong score: %d", score))case score < 60:g = "F"case score < 80:g = "C"case score < 90:g = "B"case score <= 100:g = "A"}return g
}
func main() {fmt.Println(grade(0),grade(59),grade(60),grade(82),grade(99),grade(100),grade(103),)
}

2-5 循环

for
sum := 0
for i := 1; i <= 100; i++ {
   sum += 1
}
for的条件里不需要括号
for的条件里可以省略初始条件,结束条件,递增表达式

package mainimport ("fmt""strconv""os""bufio"
)func convertToBin(n int) string {result := ""for ; n > 0; n /= 2 {lsb := n % 2result = strconv.Itoa(lsb) + result}return result
}func printFile(filename string) {file, err := os.Open(filename)if err != nil {panic(err)}scanner := bufio.NewScanner(file)for scanner.Scan() {fmt.Println(scanner.Text())}
}
//死循环
func forever() {for {fmt.Println("abc")}
}
func main() {fmt.Println(convertToBin(5), //101convertToBin(13), //1101convertToBin(72337885), //convertToBin(0), //)printFile("abc.txt")forever()
}

省略初始条件,相等于while

func convertToBin(n int) string {result := ""for ; n > 0; n /= 2 {lsb := n % 2result = strconv.Itoa(lsb) + result}return result
}

省略初始条件,相等于while

for scanner.Scan() {fmt.Println(scanner.Text())}

无限循环

for {fmt.Println("abc")}

基本语法要点回顾:

for, if后面的条件没有括号
if条件里也可以定义变量
没有while
switch不需要break,也可以直接switch多个条件

2-6 函数

函数
func eval(a, b int, op string) int

函数可返回多个值:

func div(a, b int) (int, int) {
   return a / b, a % b
}

函数返回多个值时可以起名字

仅用于非常简单的函数

对于调用者而言没有区别

func div(a, b int) (q, r int) {
     q = a / b

 r = a % b

return
}

package mainimport ("fmt""reflect""runtime""math"
)func eval(a, b int, op string) (int, error) {switch op {case "+":return a + b, nilcase "-":return a - b, nilcase "*":return a * b, nilcase "/":q, _ := div(a, b)return q, nildefault:return 0, fmt.Errorf("unsupported operation: %s", op)}
}// 13 / 3 = 4 ... 1
func div(a, b int) (q, r int) {return a / b, a % b
}
//函数式编程
func apply(op func(int, int) int, a, b int) int {p := reflect.ValueOf(op).Pointer()opName := runtime.FuncForPC(p).Name()fmt.Printf("Calling function %s with args " +"(%d, %d)\n", opName, a, b)return op(a, b)
}/*func pow(a, b int) int {return int(math.Pow(float64(a), float64(b)))
}*///可变参数列表
func sum(numbers ...int) int {s := 0for i := range numbers {s += numbers[i]}return s
}func main() {if result, err := eval(3, 4, "x"); err!= nil{fmt.Println("Error:", err)} else {fmt.Println(result)}q, r := div(13, 3)fmt.Println(q, r)fmt.Println(apply(func(a int, b int) int {return int(math.Pow(float64(a), float64(b)))}, 3, 4))fmt.Println(sum(1, 2, 3, 4, 5))}

函数式参数也可以是一个函数,也可以写一个匿名函数来调它。

函数作为参数:
func apply(op func(int, int) int, a, b int) int {
   fmt.Printf("Calling function %s with %d, %d\n",
      runtime.FuncForPC(reflect.ValueOf(op).Pointer()).Name(),
      a, b)
   return op(a, b)
}

可变参数列表:

func sumArgs(values ...int) int {
   s := 0
   for i := range values{
      sum += values[i]
   }
   return sum
}

函数语法要点回顾:

返回值类型写在最后面

可返回多个值

函数作为参数

没有默认参数,可选参数

2-7 指针
指针不能运算
var a int = 2
var pa *int = &a
*pa = 3
fmt.Println(a)
参数传递
Go语言只有值传递一种方式

拷贝一份a的地址,使用指针传递相当于引用传递的效果

Go语言中Object类型的名字叫Cache

package mainimport ("fmt"
)func swap(a, b int) (int, int){return b, a
}
func main() {a, b := 3, 4a, b = swap(a, b)fmt.Println(a, b)
}

第3章 内建容器

学习数组,切片,Map和字符串。在Go语言中,我们一般不直接使用数组,而是使用切片来管理线性表结构,它的语法类似python的list,不过更强大哦。

  1. 内建容器
package mainfunc main() {cache := make(map[string]string)cache["name"] = "ccmouse"}

3-1 数组

数量写在类型前:

var arr1 [5]int

arr2 := [3]int{1, 3, 5}

arr3 := [...]int{2, 4, 6, 8, 10}

var grid [4][5]int

数组的遍历:

numbers := [6]int{1, 3, 2, 5, 8, 4}

for i := 0; i < len(numbers); i++ {

fmt.Println(numbers[i])

}

maxi := -1

maxVlue := -1

for i, v := range numbers {

if v > maxValue {

maxi, maxValue = i, v

}

}

sum := 0

for _, v := range numbes {

sum += v

}

可通过_省略变量

不仅range,任何地方都可通过_省略变量

如果只要i,可写成for i := range numbers

为什么要用range

意义明确,美观

C++:没有类似能力

Java/Python:只能for each value,不能同时获取i, v

package mainimport "fmt"func main() {var arr1 [5]intarr2 := [3]int{1, 3, 5}arr3 := [...]int{2, 4, 6, 8, 10}//二维数组var grid [4][5]intfmt.Println(arr1, arr2, arr3)fmt.Println(grid)//遍历数组for _, v := range arr3 {fmt.Println(v)}}

数组是值类型,printArray()第0号元素被改变了,但是外面的arr1元素没有发生改变。调用printArray(arr1)时会把这5个元素作为一个整体拷贝。

package mainimport "fmt"func printArray(arr [5]int) {arr[0] = 100//遍历数组for i, v := range arr {fmt.Println(i, v)}//arr[0] = 100}func main() {var arr1 [5]intarr2 := [3]int{1, 3, 5}arr3 := [...]int{2, 4, 6, 8, 10}//二维数组var grid [4][5]intfmt.Println(arr1, arr2, arr3)fmt.Println(grid)fmt.Println("printArray(arr1)")printArray(arr1)fmt.Println("printArray(arr3)")printArray(arr3)fmt.Println("arr1 and arr3")fmt.Println(arr1, arr3)}

[10]int和[20]int是不同类型

调用func f(arr [10]int)会拷贝数组

在go语言中一般不直接使用数组,用切片。

package mainimport "fmt"func printArray(arr *[5]int) {//不需要(*arr)[0],也可以把第0个元素取出来arr[0] = 100//遍历数组for i, v := range arr {fmt.Println(i, v)}//arr[0] = 100}func main() {var arr1 [5]intarr2 := [3]int{1, 3, 5}arr3 := [...]int{2, 4, 6, 8, 10}//二维数组var grid [4][5]intfmt.Println(arr1, arr2, arr3)fmt.Println(grid)fmt.Println("printArray(arr1)")printArray(&arr1)fmt.Println("printArray(arr3)")printArray(&arr3)fmt.Println("arr1 and arr3")fmt.Println(arr1, arr3)}

3-2切片的概念

Slice(切片)不是值类型

arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}

s := arr[2:6]

s的值为[2 3 4 5]//左闭右开区间

package mainimport "fmt"func main() {arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}//s := arr[2:6]fmt.Println("arr[2:6] =", arr[2:6])fmt.Println("arr[:6] =", arr[:6])fmt.Println("arr[2:] =", arr[2:])fmt.Println("arr[:] =", arr[:])}

控制台:

arr[2:6] = [2 3 4 5]

arr[:6] = [0 1 2 3 4 5]

arr[2:] = [2 3 4 5 6 7]

arr[:] = [0 1 2 3 4 5 6 7]

package mainimport "fmt"func updateSlice(s []int) {s[0] = 100}func main() {arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}//s := arr[2:6]fmt.Println("arr[2:6] =", arr[2:6])fmt.Println("arr[:6] =", arr[:6])s1 := arr[2:6]fmt.Println("s1 =", s1)s2 := arr[:]//s2是arr的slicefmt.Println("s2 =", s2)fmt.Println("After updateSlice(s1)")updateSlice(s1)fmt.Println(s1)fmt.Println(arr)fmt.Println("After updateSlice(s2)")updateSlice(s2)fmt.Println(s2)fmt.Println(arr)fmt.Println(“Reslice”)fmt.Println(s2)s2 = s2[: 5]fmt.Println(s2)s2 = s2[2: ]fmt.Println(s2)}

控制台:

arr[2:6] = [2 3 4 5]

arr[:6] = [0 1 2 3 4 5]

s1 = [2 3 4 5]

s2 = [0 1 2 3 4 5 6 7]

After updateSlice(s1)

[100 3 4 5]

[0 1 100 3 4 5 6 7]

After updateSlice(s2)

[100 1 100 3 4 5 6 7]

[100 1 100 3 4 5 6 7]

Reslice

[100 1 100 3 4 5 6 7]//s2是整个的数组

[100 1 100 3 4]

[100 3 4]

s[0] = 10

Slice本身没有数据,是对底层array的一个view

//s[0] = 10, s[0]相等于arr的第2号

arr的值变为[0 1 10 3 4 5 6 7]

package mainimport "fmt"func printArray(arr []int) {//不需要(*arr)[0],也可以把第0个元素取出来arr[0] = 100//遍历数组for i, v := range arr {fmt.Println(i, v)}//arr[0] = 100}func main() {var arr1 [5]intarr2 := [3]int{1, 3, 5}arr3 := [...]int{2, 4, 6, 8, 10}//二维数组var grid [4][5]intfmt.Println(arr1, arr2, arr3)fmt.Println(grid)fmt.Println("printArray(arr1)")printArray(arr1[:])fmt.Println("printArray(arr3)")printArray(arr3[:])fmt.Println("arr1 and arr3")fmt.Println(arr1, arr3)}

ReSlice
s := arr[2:6]
s = s[:3]
s = s[1:]
s = arr[:]

Slice的扩展

arr := [...]int{0,1,2,3,4,5,6,7}

s1 := arr[2:6]

s2 := s1[3:5]

s1的值为?

s2的值为?

fmt.Println("Extending slice")arr[0], arr[2] =0, 2fmt.Println("arr =", arr)s1 = arr[2:6]s2 = s1[3:5]//[s1[3], s1[4]]fmt.Println("s1=", s1)fmt.Println("s2=", s2)fmt.Printf("s1=%v, len(s1)=%d, cap(s1)=%d\n", s1, len(s1), cap(s1))fmt.Printf("s2=%v, len(s2)=%d, cap(s2)=%d\n", s2, len(s2), cap(s2))fmt.Println("s1[3:6]")

s1的值为[2 3 4 5], s2的值为[5 6]

slice可以向后扩展,不可以向前扩展

s[i]不可以超越len(s),向后扩展不可以超越底层数组cap(s)

3-3 切片的操作

向Slice添加元素

arr := [...]int{0,1,2,3,4,5,6,7}

s1 := arr[2:6]

s2 := s1[3:5]

s3 := append(s2, 10)

s4 := append(s3, 11)

s5 := append(s4, 12)

s3, s4, s5的值为?arr的值为?

s3 := append(s2, 10)

s4 := append(s3, 11)

s5 := append(s4, 12)

fmt.Println("s3, s4, s5 =", s3, s4, s5)

fmt.Println("arr =", arr)

s3,  s4,  s5 = [5 6 10] [5 6 10 11] [5 6 10 11 12]

// s4 and s5 no longer view arr,而是新的arr

arr = [0 1 2 3 4 5 6 10]

添加元素时如果超越cap,系统会重新分配更大的底层数组

由于值传递的关系,必须接收append的返回值

s = append(s, val)

如何创建slice

package mainimport "fmt"func printSlice(s []int) {fmt.Printf("%v, len=%d, cap=%d\n",s, len(s), cap(s))}func main() {fmt.Println("Creating slice")var s []int// Zero value for slice is nilfor i := 0; i < 100; i++ {printSlice(s)s = append(s, 2 * i + 1)}fmt.Println(s)s1 := []int{2, 4, 6, 8}printSlice(s1)s2 := make([]int, 16)s3 := make([]int, 10, 32)printSlice(s2)printSlice(s3)fmt.Println("Coping slice")copy(s2, s1)printSlice(s2)fmt.Println("Deleting elements from slice")s2 = append(s2[:3], s2[4:]...)printSlice(s2)fmt.Println("Poping from front")front := s2[0]s2 = s2[1:]fmt.Println(front)printSlice(s2)fmt.Println("Popping from back")tail := s2[len(s2) -1]s2 = s2[:len(s2) - 1]//fmt.Println(front, tail)fmt.Println(tail)printSlice(s2)}

3-4 Map

m := map[string]string {

"name": "ccmouse",

"course": "golang",

"site": "imooc",

"quality": "notbad",

}

map[k]V,map[K1]map[K2]V

package mainimport "fmt"func main() {m := map[string]string {"name": "ccmouse","course": "golang","site": "imooc","quality": "notbad",}m2 := make(map[string]int) // m2 == empty mapvar m3 map[string]int // m3 == nilfmt.Println(m, m2, m3)fmt.Println("Traversing map")for k, v := range m {fmt.Println(k,v)}/*for k := range m {fmt.Println(k)}*//*for _, v := range m {fmt.Println(v)}*/fmt.Println("Getting values")courseName, ok := m["course"]fmt.Println(courseName, ok)//causeName, ok := m["cause"]//fmt.Println(causeName, ok)if causeName, ok := m["cause"]; ok {fmt.Println(causeName, ok)} else {fmt.Println("key does not exist")}fmt.Println("Deleteing values")name, ok := m["name"]fmt.Println(name, ok)delete(m, "name")name, ok = m["name"]fmt.Println(name, ok)}

创建:make(map[string]int)

获取元素:m[key]

key不存在时,获取Value类型的初始值

用value, ok := m[key]来判断是否存在key

Map的遍历

使用range遍历key,或者遍历key, value对

不保证遍历顺序,如需顺序,需手动对key排序

使用len获取元素个数

map的key

map使用哈希表,必须可以比较相等

除了slice,map,function的内建类型都可以作为key

Struct类型不包含上述字段,也可作为key

例:寻找最长不含有重复字符的子串

abcabcbb -> abc

bbbbb -> b

pwwkew -> wke

对于每一个字母x

lastOccurred[x]不存在,或者 < start -> 无需操作

lastOccurred[x] >= start -> 更新start

更新lastOccurred[x],更新maxLength

package mainfunc lengthOfNonRepeatingSubStr(s string) int {lastOccurred := make(map[byte]int)start := 0maxLength := 0for i, ch := range []byte(s) {//lastOccurred[ch]可能不存在,出现0//lastI, ok := lastOccurred[ch]if lastI, ok := lastOccurred[ch];ok && lastI >= start {start = lastI + 1}if i - start + 1 > maxLength {maxLength = i - start + 1}lastOccurred[ch] = i}return maxLength}
func main() {fmt.Println(lengthOfNonRepeatingSubStr("abcabcbb"))fmt.Println(lengthOfNonRepeatingSubStr("bbbbb"))fmt.Println(lengthOfNonRepeatingSubStr("pwwkew"))fmt.Println(lengthOfNonRepeatingSubStr(""))fmt.Println(lengthOfNonRepeatingSubStr("b"))fmt.Println(lengthOfNonRepeatingSubStr("abcdef"))}

3

1

3

0

1

6

3-6字符和字符串处理

rune相当于go的char

package mainfunc main() {s := "Yes我爱慕课网!"// UTF-8//fmt.Printf("%s\n", []byte(s))//fmt.Printf("%X\n", []byte(s))fmt.Println(s)for _, b := range []bytes(s) {fmt.Print("%X ", b)}fmt.Println()for i, ch := range s {//ch is a runefmt.Printf("(%d %X) ", i, ch)}fmt.Println()fmt.Println("Rune count:",utf8.RuneCountInSting(s))byte := []byte(s)for len(bytes) > 0 {ch, size := utf8.DecodeRune(bytes)bytes = bytes[size:]fmt.Printf("%c", ch)}fmt.Println()for i, ch := range []rune(s) {fmt.Printf("(%d %c) ", i, ch)}fmt.Println()}

使用range遍历pos, rune对

使用utf8.RuneCountInString获得字符数量

使用len获得字节长度

使用[]byte获得字节

支持中文

package mainfunc lengthOfNonRepeatingSubStr(s string) int {lastOccurred := make(map[rune]int)start := 0maxLength := 0for i, ch := range []rune(s) {//lastOccurred[ch]可能不存在,出现0//lastI, ok := lastOccurred[ch]if lastI, ok := lastOccurred[ch];ok && lastI >= start {start = lastI + 1}if i - start + 1 > maxLength {maxLength = i - start + 1}lastOccurred[ch] = i}return maxLength}
func main() {fmt.Println(lengthOfNonRepeatingSubStr("abcabcbb"))fmt.Println(lengthOfNonRepeatingSubStr("bbbbb"))fmt.Println(lengthOfNonRepeatingSubStr("pwwkew"))fmt.Println(lengthOfNonRepeatingSubStr(""))fmt.Println(lengthOfNonRepeatingSubStr("b"))fmt.Println(lengthOfNonRepeatingSubStr("abcdef"))fmt.Println(lengthOfNonRepeatingSubStr("这里是慕课网"))fmt.Println(lengthOfNonRepeatingSubStr("一二三二一"))}

其他字符串操作

Fields, Split, Join

Contains, Index

ToLower, ToUpper

Trim, TrimRight, TrimLeft

4-1结构体和方法

Go语言没有class,只有struct。我们来看看struct如何使用,Go语言给结构体定义类似方法或者成员函数的做法非常有特色。我们还将学习Go语言的包的概念,以及如何封装,如何扩展已有类型等。我们还将学习GOPATH和Go语言项目的目录结构,如何从网上下载依赖包等一系列项目相关的知识。我们将以“树”的结构和遍历作为贯穿本章

面向对象

type TreeNode struct {Left, Right *TreeNodeValue int}func (root *TreeNode) traverse() {if root == nil {return    }root.Left.traverse()fmt.Print(root.Value)root.Right.traverse()}

go语言仅支持封装,不支持继承和多态

go语言没有class,只有struct

结构的定义

type TreeNode struct {

Left, Right *TreeNode

Value int

}

node.go

package maintype treeNode struct {value intleft, right *treeNode}func (node *treeNode) traverse() {if node == nil {return     }node.left.traverse()node.print()node.right.traverse()}func (node treeNode) print() {fmt.Print(node.value, "")}func (node treeNode) setValue(value) {node.value = value}//没有构造函数,我们可以使用工厂函数func createNode(value int) *treeNode {return treeNode{value: vlaue}}func (node *treeNode) setValue(value) {if node == nil {fmt.Println("setting value to nil " +"node. Ignored. ")return}node.value =value}func main() {var root treeNode//fmt.Println(root)root = treeNode{value: 3}root.left = &treeNode{}root.right = &treeNode{5, nil, nil}root.right.left = new(treeNode)/*nodes := []treeNode {{value: 3},{},{6, nil, &root},}fmt.Println(nodes)*/root.left.right = createTreeNode(2)//root.print()root.right.left.setValue(4)root.traverse()root.right.left.print()fmt.Println()root.print()root.setValue(100)//pRoot := &root//pRoot.print()var pRoot *treeNodepRoot.setValue(200)//pRoot.print()pRoot = &rootpRoot.setValue(300)pRoot.print()}

结构的创建

root = treeNode{value: 3}

root.left = &treeNode{}

root.right = &treeNode{5, nil, nil}

root.right.left = new(treeNode)

不论地址还是结构本身,一律使用.来访问成员

func createNode(value int) *treeNode {return treeNode{value: vlaue}}root.left.right = createTreeNode(2)

使用自定义工厂函数

注意返回了局部变量的地址!

结构创建在堆上还是栈上?

不需要知道

我们建立了

为结构定义方法

func (node treeNode) print() {

fmt.Print(node.value)

}

显示定义和命名方法接受者

使用指针作为方法接受者

func (node treeNode) setValue(value) {

node.value = value

}

只有使用指针才可以改变结构内容

nil指针也可以调用方法!

值接收者vs指针接收者

结构过大也考虑使用指针接收者

一致性:如有指针接收者,最好都是指针接受者

值接收者是go语言特有

值/指针接收者均可接收值/指针

如何扩充系统类型或者别人的类型

定义别名

使用组合

go语言的ducktyping

同事需要Readable,Appendable怎么办?(apache polygene)

同时具有python,c++的duck typing的灵活性

又具有java的类型检查

4-2 包和封装

针对包来说

名字一般使用CamelCase
首字母大写:public 公有
首字母小写:private 私有
包:

每个目录一个包
main包包含可执行入口(main 函数)
为结构定义的方法必须放在同一个包内
可以是不同的文件
package main
 
import "fmt"
 
func main() {
    //one:=0
    one,two:=1,2
    one,two=two,one //交换变量值
    fmt.Println(one,two)
}
如何扩充系统类型或者别人的类型

定义别名
使用组合
queue.go

package queue
 
type Queue []int
 
func (q *Queue) Push(v int) {
    *q = append(*q, v)
}
 
func (q *Queue) Pop() int {
    head := (*q)[0]
    *q = (*q)[1:]
    return head
}
 
func (q *Queue) IsEmpty() bool {
    return len(*q) == 0
}
mian.go 文件

package main
 
import (
    "../../queue"
    "fmt"
)
 
func main() {
    q := queue.Queue{1} //队列先进先出
 
    q.Push(2)
    q.Push(3)
    fmt.Println(q.Pop())     //1
    fmt.Println(q.Pop())     //2
    fmt.Println(q.IsEmpty()) //false
    fmt.Println(q.Pop())     //3
    fmt.Println(q.IsEmpty()) //true
 
}
四.gopath环境变量

Go 环境变量
Go 开发环境依赖于一些操作系统环境变量,你最好在安装 Go 之间就已经设置好他们。如果你使用的是 Windows 的话,你完全不用进行手动设置,Go 将被默认安装在目录 c:/go 下。这里列举几个最为重要的环境变量:

GOROOT表示Go在你的电脑上的安装位置,它的值一般都是GOROOT表示Go在你的电脑上的安装位置,它的值一般都是HOME/go,当然,你也可以安装在别的地方。
GOARCH表示目标机器的处理器架构,它的值可以是386、amd64或arm。GOARCH表示目标机器的处理器架构,它的值可以是386、amd64或arm。GOOS 表示目标机器的操作系统,它的值可以是 darwin、freebsd、linux 或 windows。
GOBIN表示编译器和链接器的安装位置,默认是GOBIN表示编译器和链接器的安装位置,默认是GOROOT/bin,如果你使用的是 Go 1.0.3 及以后的版本,一般情况下你可以将它的值设置为空,Go 将会使用前面提到的默认值。

目标机器是指你打算运行你的 Go 应用程序的机器。

所有库和第三方库都放在同一GOPATH下。

go bulid 编译
go install 产生pkg文件和可执行文件
go run 直接编译运行
面向接口(五)

这一章我们从duck typing的概念开始学起,还将探讨其他语言中对duck typing的支持,由此引出接口的概念。我们将深入理解Go语言接口的内部实现以及使用接口实现组合的模式。

大黄鸭是鸭子吗?

  • 传统类型系统:脊索动物门,脊椎动物亚门,鸟纲雁形目…  不是
  • duck typing:是鸭子
  • 概念:“像鸭子走路,像鸭子叫(长得像鸭子),那么就是鸭子”
  • 描述事物的外部行为而非内部结构
  • 严格说 go 属于结构化类型系统,类似 duck typing

Python 中的 duck typing

1

2

def download(retriever):

return retriever.get("www.baidu.com")

  • 运行时才知道传入的 retriever 有没有 get
  • 需要注释来说明接口

C++中的 duck typing

1

2

3

4

template <class R>

string download(const R& retriever) {

return retriever.get("www.baidu.com")

}

  • 编译时才知道传入的 retriever 有没有 get
  • 需要注释来说明接口

Java 中的类似代码

1

2

3

4

<R extends Retriever>

String download(R r) {

return r.get("www.baidu.com")

}

  • 传入的参数必须实现 Retriever 接口
  • 不是 duck typing
  • 同时需要 Readable, Appendable 怎么办?(apache polygene)

Go 语言的 duck typing

  • 同时具有 Python, C++的 duck typing 的灵活性
  • 又具有 Java 的类型检查

接口的定义和实现

接口的定义

使用者(download)→实现者(retriever)

  • 接口由使用者定义

1

2

3

4

5

6

7

type Retriever interface {

Get(url string) string

}

func download(r Retriever) string {

return r.Get("http://www.baidu.com")

}

接口的实现

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

接口的值类型

接口变量里面有什么

接口变量

  • 实现者的类型
  • 实现者的值或实现者的指针

接口变量里面有什么

  • 接口变量自带指针
  • 接口变量同样采用值传递,几乎不需要使用接口的指针
  • 指针接收者实现只能以指针方式使用;值接收者都可

查看接口变量

  • 表示任何类型:interface{}
  • Type Assertion
  • Type Switch

接口的组合

1

2

3

4

type ReaderWriter interface {

Reader

Writer

}

常用系统接口

  • Stringer
  • Reader/Writer

一.duck typing概念

严格说go属于结构化类型系统,类似duck typing 
描述事物的外部行为而非内部结构
同时需要Readable,Appendable 怎么办?(apache polygene)
同时具有python,C++的duck typing的灵活性
又具有Java的类型检查.
1.接口的定义和实现

  • 接口由使用者定义
  • 接口的实现是隐式的
  • 只要实现接口里的方法

2.接口变量里面有什么

  • 接口变量自带指针
  • 接口变量同样采用值传递,几乎不需要使用接口的指针
  • 指针接受者实现只能以指针方式使用,值接受者都可以

3.查看接口变量

  • 表示任何类型;interface{}
  • Type Assertion
  • Type Switch

函数式编程(六)

函数与闭包

函数式编程 vs. 函数指针

  • 函数是一等公民:参数、变量、返回值都可以是函数
  • 高阶函数
  • 函数 → 闭包

“正统”函数式编程

  • 不可变性:不能有状态,只有常量和函数
  • 函数只能有一个参数
  • 本课程不作上述规定

闭包

Python 中闭包

1

2

3

4

5

6

7

8

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

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

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语言闭包的应用

  • 例一:斐波那契数列
  • 例二:为函数实现接口
  • 例三:使用函数来遍历二叉树

总结

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

函数式编程 & 函数指针

函数是一等公民:参数,变量,返回值都可以是函数
高阶函数
函数--> 闭包
1
2
3
示例:adder.go

package main

import "fmt"

func adder()func(int) int  { //闭包
    sum := 0
    return func(v int) int {  
        sum += v
        return sum
    }
}

func main()  {
    a := adder()
    for i:= 0;i < 10; i++{
        fmt.Printf("0 + 1 + 2 + ... + %d = %d \n",i,a(i))
    }
}

go语言闭包的应用

实例一:斐波那契数列

package main

import "fmt"

func fibonacci()func() int {
    a, b := 0, 1
    return func() int {
        a, b = b, a + b
        return a
    }
}

func main() {
    f := fibonacci()

fmt.Println(f()) //1
    fmt.Println(f()) //1
    fmt.Println(f()) //2
    fmt.Println(f()) //3
    fmt.Println(f()) //5
    fmt.Println(f()) //8
    fmt.Println(f()) //13
    fmt.Println(f()) //21
}

实例二:为函数实现接口

package main

import (
    "bufio"
    "fmt"
    "io"
    "strings"
)

func fibonacci() intGen {
    a, b := 0, 1
    return func() int {
        a, b = b, a+b
        return a
    }
}

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)
    //TODO:如果p太小,则需要实现缓存
    return strings.NewReader(s).Read(p)
}

func printFileContents(reader io.Reader) {
    scanner := bufio.NewScanner(reader)
    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }
}

func main() {
    f := fibonacci()
    printFileContents(f)
    //fmt.Println(f()) //1
    //fmt.Println(f()) //1
    //fmt.Println(f()) //2
    //fmt.Println(f()) //3
    //fmt.Println(f()) //5
    //fmt.Println(f()) //8
    //fmt.Println(f()) //13
    //fmt.Println(f()) //21
}

扩展:

二叉树遍历(前序、中序、后序、层次遍历、深度优先、广度优先)

二叉树是一种非常重要的数据结构,很多其它数据结构都是基于二叉树的基础演变而来的。对于二叉树,有深度遍历和广度遍历,深度遍历有前序、中序以及后序三种遍历方法,广度遍历即我们平常所说的层次遍历。因为树的定义本身就是递归定义,因此采用递归的方法去实现树的三种遍历不仅容易理解而且代码很简洁,而对于广度遍历来说,需要其他数据结构的支撑,比如堆了。所以,对于一段代码来说,可读性有时候要比代码本身的效率要重要的多。

四种主要的遍历思想为:

前序遍历:根结点 ---> 左子树 ---> 右子树

中序遍历:左子树---> 根结点 ---> 右子树

后序遍历:左子树 ---> 右子树 ---> 根结点

层次遍历:只需按层次遍历即可

例如,求下面二叉树的各种遍历

前序遍历:1  2  4  5  7  8  3  6

中序遍历:4  2  7  5  8  1  3  6

后序遍历:4  7  8  5  2  6  3  1

层次遍历:1  2  3  4  5  6  7  8

错误处理和资源管理(七)

Go语言独特的defer/panic/recover,以及错误机制,在社区有着广泛的争论。我们来深入理解Go语言的错误处理机制,看看Go语言如何区分错误以及异常。最后,我们实现一个Web应用微型项目,采用商业服务的错误处理思路,结合函数式编程,来演示Go语言错误

defer调用

  • 确保调用在函数结束时发生
  • 参数在 defer 语句时计算
  • 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)

}

}

服务器统一出错处理

  • 如何实现统一错误处理逻辑

panic和recover

panic

  • 停止当前函数执行
  • 一直向上返回,执行每一层的 defer
  • 如果没有遇见 recover,程序退出

recover

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

服务器统一出错处理2

error vs panic

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

错误处理综合示例

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

Go 是怎么处理普通错误的呢?通过在函数和方法中返回错误对象作为它们的唯一或最后一个返回值——如果返回 nil,则没有错误发生——并且主调(calling)函数总是应该检查收到的错误。

永远不要忽略错误,否则可能会导致程序崩溃!!

处理错误并且在函数发生错误的地方给用户返回错误信息:照这样处理就算真的出了问题,你的程序也能继续运行并且通知给用户。panic and recover 是用来处理真正的异常(无法预测的错误)而不是普通的错误。

产生错误的函数会返回两个变量,一个值和一个错误码;如果后者是 nil 就是成功,非 nil 就是发生了错误。
为了防止发生错误时正在执行的函数(如果有必要的话甚至会是整个程序)被中止,在调用函数后必须检查错误。
Go 有一个预先定义的 error 接口类型

type error interface { 
    Error() string
}
1
2
3
errors 包中有一个 errorString 结构体实现了 error 接口。当程序处于错误状态时可以用 os.Exit(1)来中止运行。

任何时候当你需要一个新的错误类型,都可以用 errors(必须先 import)包的 errors.New 函数接收合适的错误信息来创建,像下面这样:

err := errors.New(“math - square root of negative number”)
1
所有的例子都遵循同一种命名规范:错误类型以 “Error” 结尾,错误变量以 “err” 或 “Err” 开头。

通常你想要返回包含错误参数的更有信息量的字符串,例如:可以用 fmt.Errorf() 来实现:它和 fmt.Printf() 完全一样,接收有一个或多个格式占位符的格式化字符串和相应数量的占位变量。和打印信息不同的是它用信息生成错误对象。

panic 和 recover
panic:

停止当前函数执行
一直向上返回,执行每一层的defer
如果没有遇见recover,程序退出
recover:

仅在defer调用中使用
获取panic的值
如果无法处理,可重新panic
当发生像数组下标越界或类型断言失败这样的运行错误时,Go 运行时会触发运行时 panic,伴随着程序的崩溃抛出一个runtime.Error 接口类型的值。这个错误值有个 RuntimeError() 方法用于区别普通错误。

panic 可以直接从代码初始化:当错误条件(我们所测试的代码)很严苛且不可恢复,程序不能继续运行时,可以使用panic 函数产生一个中止程序的运行时错误。panic 接收一个做任意类型的参数,通常是字符串,在程序死亡时被打印出来。Go 运行时负责中止程序并给出调试信息。

标准库中有许多包含 Must 前缀的函数,像 regexp.MustComplie 和 template.Must;当正则表达式或模板中转入的转换字符串导致错误时,这些函数会 panic。

不能随意地用 panic 中止程序,必须尽力补救错误让程序能继续执行。

recover 只能在 defer 修饰的函数中使用:用于取得 panic 调用中传递过来的错误值,如果是正常执行,调用 recover 会返回 nil,且没有其它效果。

package main

import "fmt"

func main() {
    tryRecover()
}

func tryRecover() {
    defer func() {
        r := recover()
        if err, ok := r.(error); ok {
            fmt.Println("Error occurred:", err)
        } else {
            panic(fmt.Sprintf("i don't what to do %v", r))
        }
    }()

//b := 0
    //a := 5 / b
    //fmt.Println(a)
    panic(132)
}

一.defer调用:实现资源管理

确保调用在函数结束时发生
参数在defer语句时计算
defer列表为后进先出
何时使用defer调用

Open/Close
Lock/Unlock
PrintHeader/PrintFooter
package main
 
import "fmt"
 
func tryDefer(){
    defer fmt.Println(1)
    defer fmt.Println(2)//defer 相当于栈:先进后出
    fmt.Println(3)
    //结果:3 2 1
}
func main() {
    tryDefer()
}
package main
 
import (
    "../../functional/fib"
    "bufio"
    "fmt"
    "os"
)
 
func tryDefer() {
    defer fmt.Println(1)
    defer fmt.Println(2) //defer 相当于栈:先进后出
    fmt.Println(3)
    //结果:3 2 1
    //return
    panic("error occurred")
    fmt.Println(4)
    //3
    //2
    //1
    //panic: error occurred
}
//参数在defer语句时计算
func tryDefer2() {
    for i := 0; i < 100; i++ {
        defer fmt.Println(i) //
        if i == 10 {
            panic("print too many")
        }
    }
}
 
func writeFile(filename string) {
    file, err := os.Create(filename)
    if err != nil {
        panic(err)
    }
    defer file.Close()
 
    writer := bufio.NewWriter(file)
    defer writer.Flush()
 
    f := fib.Fibonacci()
    for i := 0; i < 10; i++ {
        fmt.Fprintln(writer, f())
    }
}
 
func main() {
    //tryDefer()
    tryDefer2()
    writeFile("fib.txt")
}
二.错误处理理念

func writeFile(filename string) {
    //file, err := os.Create(filename)
    file, err := os.OpenFile(filename, os.O_EXCL|os.O_CREATE, 0666) //O_EXCL 如果文件存在的话打开不了
    // panic: open fib.txt: file exists
    err = errors.New("this is a custum error")
    if err != nil {
        //fmt.Println("Error:",err.Error())
        if pathError, ok := err.(*os.PathError); !ok {
            panic(err)
        } else {
            fmt.Printf("%s ,%s ,%s\n ", pathError.Op, pathError.Path, pathError.Err)
        }
        return
    }
    defer file.Close()
 
    writer := bufio.NewWriter(file)
    defer writer.Flush()
 
    f := fib.Fibonacci()
    for i := 0; i < 10; i++ {
        fmt.Fprintln(writer, f())
    }
}
三.服务器统一出错处理

web.go

package main
 
import (
    "./filelisting"
    "log"
    "net/http"
    "os"
)
 
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)
        if err != nil {
            log.Printf("Error handing request:%s",err.Error()) //2020/08/29 17:17:36 Error handing request:open fib2.txt: permission denied
            code := http.StatusOK
            switch {
            case os.IsNotExist(err):
                //http.Error(writer, http.StatusText(http.StatusNotFound), http.StatusNotFound)
                code = http.StatusNotFound
            case os.IsPermission(err):
                code = http.StatusForbidden
            default:
                code = http.StatusInternalServerError
            }
            http.Error(writer, http.StatusText(code), code)
        }
    }
}
func main() {
    http.HandleFunc("/list/", errWrapper(filelisting.HandleFileList))
 
    err := http.ListenAndServe(":8081", nil)
    if err != nil {
        panic(err)
    }
}
handler.go

package filelisting
 
import (
    "io/ioutil"
    "net/http"
    "os"
)
 
func HandleFileList(writer http.ResponseWriter, request *http.Request) error {
    path := request.URL.Path[len("/list/"):]
    file, err := os.Open(path)
    if err != nil {
        //panic(err)
        /*http.Error(writer,
            err.Error(),
            http.StatusInternalServerError)
        return*/
        return err
    }
    defer file.Close()
 
    all, err := ioutil.ReadAll(file)
    if err != nil {
        //panic(err)
        return err
    }
    writer.Write(all)
    return nil
}
四.panic和recover

panin功能作用

停止当前函数执行
一直向上返回,执行每一层的defer
如果没有遇见recover,程序退出
recover功能作用

仅在defer调用中使用
获取panic的值
如果无法处理,可重新panic
package main
 
import (
    "fmt"
)
 
func tryRecover() {
    defer func() {
        r := recover()
        if err, ok := r.(error); ok {
            fmt.Println("Error occurred:", err)
        } else {
            fmt.Println(r)
        }
    }()
 
    //panic(errors.New("this is an error")) //Error occurred: this is an error,下面不执行,panic停止当前函数执行
 
    b := 0
    a := 5 / b
    fmt.Println(a) //Error occurred: runtime error: integer divide by zero
}
func main() {
    tryRecover()
}
五.服务器统一出错处理

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

总结:panic 会导致栈被展开直到 defer 修饰的 recover() 被调用或者程序中止。

log 包实现了简单的日志功能:默认的 log 对象向标准错误输出中写入并打印每条日志信息的日期和时间。除了 Println 和Printf 函数,其它的致命性函数都会在写完日志信息后调用 os.Exit(1),那些退出函数也是如此。而 Panic 效果的函数会在写完日志信息后调用 panic;可以在程序必须中止或发生了临界错误时使用它们,就像当 web 服务器不能启动时那样

测试与性能调优(八)

Go语言的测试不同于其他如junit,Go语言采用“表格驱动测试”的理念。我们将学习和体会这样的理念,并用Go语言的测试支持库来实践表格驱动测试,并做代码覆盖和性能检测,通过內建的性能调优工具来优化我们之前的算法。最后演示了对http服务器的多种粒度的测试。

测试

Debugging Sucks! Testing Rocks!

传统测试 vs 表格驱动测试

传统测试

1

2

3

4

5

6

7

8

@Test public void testAdd() {

asserEquals(3, add(1, 2));

asserEquals(2, add(0, 2));

asserEquals(0, add(0, 0));

asserEquals(0, add(-1, 1));

asserEquals(Integer.MIN_VALUE, add(1, Integer.MAX_VALUE));

}

  • 测试数据和测试逻辑混在一起
  • 出错信息不明确
  • 一旦一个数据出错测试全部结束

表格驱动测试

  • 分离的测试数据和测试逻辑
  • 明确的出错信息
  • 可以部分失败
  • go 语言的语法使得我们更易实践表格驱动测试

代码覆盖率和性能测试

代码覆盖率

1

2

go test -coverprofile=c.out

go tool cover -html=c.out

性能测试(Benchmark)

1

go test -bench .

使用pprof进行性能调优

1

2

3

4

5

6

go test -bench . -cpuprofile cpu.out

go tool pprof cpu.out

# 进入 pprof 交互式命令行,最简单的为输入 web生成图形

#  Failed to execute dot. Is Graphviz installed? Error: exec: "dot": executable file not found in $PATH

brew install graphviz

http 测试

  • 通过使用假的 Request/Response(TestErrWrapper)
  • 通过起服务器(TestErrWrapperInServer)

生成文档和示例代码

文档

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

1

2

3

go doc <pkg>

# 网页文档

godoc -http :6060

名为 testing 的包被专门用来进行自动化测试,日志和错误报告。并且还包含一些基准测试函数的功能。
对一个包做(单元)测试,需要写一些可以频繁(每次更新后)执行的小块测试单元来检查代码的正确性。于是我们必须写一些 Go 源文件来测试代码。测试程序必须属于被测试的包,并且文件名满足这种形式 *_test.go,所以测试代码和包中的业务代码是分开的。

_test 程序不会被普通的 Go 编译器编译,所以当放应用部署到生产环境时它们不会被部署;只有 gotest 会编译所有的程序:普通程序和测试程序。
测试函数必须有这种形式的头部:

func TestAbcde(t *testing.T)
1
T 是传给测试函数的结构类型,用来管理测试状态,支持格式化测试日志,如 t.Log,t.Error,t.ErrorF 等。在函数的结尾把输出跟想要的结果对比,如果不等就打印一个错误。成功的测试则直接返回。

运行 go test 来编译测试程序,并执行程序中所有的 TestZZZ 函数。如果所有的测试都通过会打印出 PASS。

gotest 可以接收一个或多个函数程序作为参数,并指定一些选项。

结合 --chatty 或 -v 选项,每个执行的测试函数以及测试状态会被打印。

例如:

go test fmt_test.go --chatty
=== RUN fmt.TestFlagParser
--- PASS: fmt.TestFlagParser
=== RUN fmt.TestArrayPrinter
--- PASS: fmt.TestArrayPrinter
...

testing 包中有一些类型和函数可以用来做简单的基准测试;测试代码中必须包含以 BenchmarkZzz 打头的函数并接收一个*testing.B 类型的参数,比如:

func BenchmarkReverse(b *testing.B) { 
    ...
}

命令 go test –test.bench=.* 会运行所有的基准测试函数;代码中的函数会被调用 N 次(N是非常大的数,如 N = 1000000),并展示 N 的值和函数执行的平均时间,单位为 ns(纳秒,ns/op)。如果是用 testing.Benchmark 调用这些函数,直接运行程序即可。

用 go test 调试
如果代码使用了 Go 中 testing 包的基准测试功能,我们可以用 gotest 标准的 -cpuprofile 和 -memprofile 标志向指定文件写入 CPU 或 内存使用情况报告。

使用方式:go test -x -v -cpuprofile=prof.out -file x_test.go

编译执行 x_test.go 中的测试,并向 prof.out 文件中写入 cpu 性能分析信息。

用 pprof 调试
你可以在单机程序 progexec 中引入 runtime/pprof 包;这个包以 pprof 可视化工具需要的格式写入运行时报告数据。对于 CPU 性能分析来说你需要添加一些代码:

var cpuprofile = flag.String(“cpuprofile”, “”, “write cpu profile to file”)

func main() {
    flag.Parse()
    if *cpuprofile != “” {
        f, err := os.Create(*cpuprofile)
        if err != nil {
            log.Fatal(err)
        }
        pprof.StartCPUProfile(f)
        defer pprof.StopCPUProfile()
    }
...

代码定义了一个名为 cpuprofile 的 flag,调用 Go flag 库来解析命令行 flag,如果命令行设置了 cpuprofile flag,则开始 CPU 性能分析并把结果重定向到那个文件。(os.Create 用拿到的名字创建了用来写入分析数据的文件)。这个分析程序最后需要在程序退出之前调用 StopCPUProfile 来刷新挂起的写操作到文件中;我们用 defer 来保证这一切会在 main 返回时触发。

生成文档:godoc -http :6060
查看文档:go doc

Goroutine并发编程(九)

讲解Goroutine,协程的概念,以及背后的Go语言调度器。

Goroutine

协程 Coroutine

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

1

go run -race  xxx.go // 检测数据访问冲突

Subroutines are special cases of more general program components, called coroutines.

子进程是协程的一个特例

普通函数:线程  main ➝ doWork

协程:线程(可能) main ⟺ doWork

其它语言中的协程

  • C++:Boost.Corouting
  • Java:不支持
  • Python:使用 yield 关键字实现协程,Python 3.5加入了 async def 对协程原生支持

goroutine 的定义

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

go routine 可能的切换点

  • I/0, select
  • channel
  • 等待锁
  • 函数调用(有时)
  • runtime.Gosched()
  • 只是参考,不能保证切换,不能保证在其他地方不切换

一.协程 coroutine

轻量级"线程":并发执行一些任务,
非抢占式多任务处理,由协程主动交出控制权:
编译器/解释器/虚拟机层面的多任务
多个协程可能在一个或多个线程上运行:由调度器决定
线程任何时候都可以被操作系统切换,抢占式任务处理,没有控制权,随时被操作系统切换.

执行go文件: go run  goroutine.go

检测数据访问的冲突  go run -race goroutine.go

package mainimport ("fmt""runtime""time"
)func main() {var sum [10]intfor i := 0; i < 10; i++ { //1000个并发执行go func(ii int) {for {//fmt.Printf("Hello from goroutine %d \n", a)sum[ii]++runtime.Gosched() //手动交出控制权}}(i)}time.Sleep(time.Microsecond)fmt.Println(sum)
}

二.go语言的调度器

python中协程

使用yield关键字实现协程
python3.5加入了async def 对协程原生支持
go语言中协程 :

goroutine的定义

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

I/o,select
channel
等待锁
函数调用(有时)
runtime.Gosched()
只是参考,不能保证切换,不能保证在其他地方不切换
子程序是协程的一个特例

___go_build_ 25.6  00:15.06 8/2 总线程8个,活动线程2个,不会超过电脑物理核数

  • channel
  • buffered channel
  • range
  • 理论基础:Communication Sequential Process (CSP)
  • Don’t communicate by sharing memory; share memory by communicating.
  • 不要通过共享内存来通信;通过通信来共享内存

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

  • 以及 WaitGroup的使用

例二:使用 Channel 来实现树的遍历

例三:使用 Select 来进行调度

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

传统同步机制

  • WaitGroup
  • Mutex
  • Cond

Channel是Goroutine之间通信的桥梁,它和函数一样是一等公民。在介绍完Channel的语法及运行方式后,我们将采用数个例题来演示Go语言并发编程中最常见的任务极其解决模式。

一.channel

channel
buffered channel
range.由发送方结束发送
理论基础:communication sequential process(csp)
不要通过共享内存来通信;通过通信来共享内存
package main
 
import (
    "fmt"
    "time"
)
 
func chanDemo() {
    c := make(chan int)
    go func() {
        for {
            n := <-c
            fmt.Println(n)
            //Output:
            // 1
            //2
        }
    }()
    c <- 1
    c <- 2
    time.Sleep(time.Microsecond)
}
 
func main() {
    chanDemo()
}
package main
 
import (
    "fmt"
    "time"
)
 
func worker(id int, c chan int) {
    for n := range c {
        //n := <-c //收数据
        //n, ok := <-c
        //if !ok {
        //    break
        //}
 
        fmt.Printf("worker %d received %d\n", id, n)
 
        //fmt.Println(n)
        //Output:
        // 1
        //2
    }
 
}
func createWorker(id int, ) chan<- int { //返回channel
    c := make(chan int)
    go worker(id, c)
    return c
}
func chanDemo() {
    //var c chan int //变量c 是个chanel 里面内容是int
    //c := make(chan int)
 
    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
    }
 
    //c <- 1  //发数据
    //c <- 2
    time.Sleep(time.Microsecond)
}
 
func bufferedChannel() {
    c := make(chan int, 3)
    go worker(0, c)
    c <- 'a'
    c <- 'b'
    c <- 'c'
    c <- 'd'
    time.Sleep(time.Microsecond)
}
func channelClose() { //发送方通知接收方
    c := make(chan int, 3)
    go worker(0, c)
    c <- 'a'
    c <- 'b'
    c <- 'c'
    c <- 'd'
    close(c)
    time.Sleep(time.Microsecond)
}
func main() {
    fmt.Println("channel ad first-class citizen")
    chanDemo()
 
    fmt.Println("Buffered channel")
    bufferedChannel()
 
    fmt.Println("Channel close and range")
    channelClose()
}
二.传统同步机制

waitGroup :等待组
Mutex 互斥
Cond
package main
 
import (
    "fmt"
    "sync"
    "time"
)
 
type atomicInt struct {
    value int
    lock sync.Mutex
}
 
func (a *atomicInt)increment()  {
    a.lock.Lock()
    defer a.lock.Unlock()
    a.value++
}
 
func (a *atomicInt)get()int  {
    a.lock.Lock()
    defer a.lock.Unlock()
    return a.value
}
 
func main() {
    var a atomicInt
    a.increment()
    go func() {
        a.increment()
    }()
    time.Sleep(time.Second)
    fmt.Println(a.get())
}

Go 语言为构建并发程序的基本代码块是 协程 (goroutine) 与通道 (channel)

一个进程由一个或多个操作系统线程组成,这些线程其实是共享同一个内存地址空间的一起工作的执行体。几乎所有’正式’的程序都是多线程的,以便让用户或计算机不必等待,或者能够同时服务多个请求(如 Web 服务器),或增加性能和吞吐量(例如,通过对不同的数据集并行执行代码)。一个并发程序可以在一个处理器或者内核上使用多个线程来执行任务,但是只有同一个程序在某个时间点同时运行在多核或者多处理器上才是真正的并行

通道

Go 使用 channels 来同步协程

协程是轻量的,比线程更轻:使用 4K 的栈内存就可以在堆中创建它们。
栈的管理是自动的,但不是由垃圾回收器管理的,而是在协程退出后自动释放

协程是通过使用关键字 go 调用(执行)一个函数或者方法来实现的(也可以是匿名或者 lambda 函数)。这样会在当前的计算过程中开始一个同时进行的函数,在相同的地址空间中并且分配了独立的栈,比如:go sum(bigArray),在后台计算总和。

在一个协程中,比如它需要进行非常密集的运算,你可以在运算循环中周期的使用 runtime.Gosched():这会让出处理器,允许运行其他协程;它并不会使当前协程挂起,所以它会自动恢复执行。使用 Gosched() 可以使计算均匀分布,使通信不至于迟迟得不到响应。

使用通道进行协程间通信
工厂的传送带是个很有用的例子。一个机器(生产者协程)在传送带上放置物品,另外一个机器(消费者协程)拿到物品并打包。

通道服务于通信的两个目的:值的交换,同步的,保证了两个计算(协程)任何时候都是可知状态。

通常使用这样的格式来声明通道:var identifier chan datatype

未初始化的通道的值是nil。

所以通道只能传输一种类型的数据,比如 chan int 或者 chan string,所有的类型都可以用于通道,空接口 interface{} 也可以。甚至可以(有时非常有用)创建通道的通道。

通道实际上是类型化消息的队列:使数据得以传输。
通道也是引用类型,所以我们使用 make() 函数来给它分配内存。这里先声明了一个字符串通道 ch1,然后创建了它(实例化):

var ch1 chan string
ch1 = make(chan string)
1
2
当然可以更短: ch1 := make(chan string)。

这里我们构建一个int通道的通道: chanOfChans := make(chan int)。

或者函数通道:funcChan := chan func()

通信操作符 <-
这个操作符直观的标示了数据的传输:信息按照箭头的方向流动

http及其他标准库(十一)

Go语言中非常重要而且封装良好的http标准库,回顾并实现http客户端和服务器。

http

  • 使用 http 客户端发送请求
  • 使用 http.Client 控制请求头部等
  • 使用 httputil 简化工作

http 服务器的性能分析

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

    1

    2

    3

    4

    # 内存使用情况

    go tool pprof http://localhost:6060/debug/pprof/heap

    # 30秒 CPU使用情况

    go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30

其它标准库

  • bufio
  • log
  • encoding/json
  • regexp
  • time
  • strings/math/rand

文档

  • godoc -http :8888
  • https://studygolang.com/pkgdoc

第三方 http 框架

  • gin-gonic

    • middleware的使用
    • context的使用

一.HTTP标准库

使用http客户端发送请求
使用http.client控制请求头部等
使用httputil简化工作
package main
 
import (
    "fmt"
    "net/http"
    "net/http/httputil"
)
 
func main() {
    resp,err:=http.Get("http://www.imooc.com")
    if err!=nil {
        panic(err)
    }
    defer resp.Body.Close()
    s,err:=httputil.DumpResponse(resp,true)
    if err !=nil{
        panic(err)
    }
    fmt.Printf("%s\n",s)
}
升级版

package main
 
import (
    "fmt"
    "net/http"
    "net/http/httputil"
)
 
func main() {
    request,err:=http.NewRequest(http.MethodGet,"http://www.imooc.com",nil)
    request.Header.Add("User-Agent"," Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1")
 
    client:=http.Client{
        Transport:     nil,//设置代理服务器
        CheckRedirect: func( //重定向从这里过
            req *http.Request,
            via []*http.Request) error {
                fmt.Println("redirect:",req)
            return nil
        },
        Jar:           nil,//模拟登陆,设置cookie
        Timeout:       0,
    }
    resp,err:=client.Do(request)
    //resp,err:=http.DefaultClient.Do(request)
    //resp,err:=http.Get("http://www.imooc.com")
    if err!=nil {
        panic(err)
    }
    defer resp.Body.Close()
    s,err:=httputil.DumpResponse(resp,true)
    if err !=nil{
        panic(err)
    }
    fmt.Printf("%s\n",len(s))
}
二.http服务器的性能分析

import _"net/http/pprof"
访问/debug/pprof
使用 go tool pprof分析性能
import (
    "./filelisting"
    "log"
    "net/http"
    "os"
    _ "net/http/pprof"  //库前加下划线,引入库,使有pprof的能力
)

浏览器访问:file:///private/var/folders/w9/l38fmd696n95tmrt4pf980vm0000gn/T/pprof001.svg

标准库文档:https://studygolang.com/pkgdoc

或者使用:godoc -http localhost:6060,在本地查看

迷宫的广度优先搜索(十二)

这章我们将综合运用学过的知识实现一个广度优先算法来解迷宫,为接下来的实战项目做好技术和算法上的准备。广度优先算法不仅是面试和工作中常用的技术,而且实现上相比大部分其它算法更为复杂,是检验是否熟练掌握一门语言的经典例题。让我们来试一试吧。

广度优先算法

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

探索顺序: 上左下右

节点三种状态:

已经发现,但没有探索过 
已经发现,并探索完成
没有发现
结束条件:(1)走到终点  (2)走到队列为空

maze.go读取文件

package main
 
import (
    "fmt"
    "os"
)
 
func readMaze(filename string) [][]int {
    file, err := os.Open(filename)
    if err != nil {
        panic(err)
    }
 
    var row, col  int
    fmt.Fscanf(file, "%d %d", &row, &col)
 
    maze := make([][]int, row)
    for i := range maze {
        maze[i] = make([]int, col)
        for j := range maze[i] {
            fmt.Fscanf(file, "%d", &maze[i][j])
        }
    }
    return maze
 
}
 
func main() {
    maze:=readMaze("maze/maze.in")
    for _,row:= range maze{
        for _,val:=range row {
            fmt.Printf("%d ",val)
        }
        fmt.Println()
    }
}
maze.in文件

6 5
0 1 0 0 0
0 0 0 1 0
0 1 0 1 0
1 1 1 0 0
0 1 0 0 1
0 1 0 0 0
广度优先算法代码

package main
 
import (
    "fmt"
    "os"
)
 
type point struct {
    i, j int
}
 
var dirs = [4]point{
    {-1, 0}, {0, -1}, {1, 0}, {0, 1}}
 
func (p point) add(r point) point {
    return point{p.i + r.i, p.j + r.j}
}
 
func (p point) at(grid [][]int) (int, bool) {
    if p.i < 0 || p.i >= len(grid) {
        return 0, false
    }
    if p.j < 0 || p.j >= len(grid[p.i]) {
        return 0, false
    }
    return grid[p.i][p.j], true
}
 
func walk(maze [][]int, start, end point)[][]int {
    steps := make([][]int, len(maze))
    for i := range steps {
        steps[i] = make([]int, len(maze[i]))
    }
 
    //队列
    Q := []point{start}
    for len(Q) > 0 {
        cur := Q[0]
        Q = Q[1:]
 
        if cur==end {
            break
        }
 
        for _, dir := range dirs {
            next := cur.add(dir)
 
            //maze at next is 0
            //and steps at next is 0
            //and next !=start
            val, ok := next.at(maze)
            if !ok || val == 1 {
                continue
            }
            val, ok = next.at(steps)
            if !ok || val != 0 {
                continue
            }
            if next == start {
                continue
            }
 
            curSteps, _ := cur.at(steps)
            steps[next.i][next.j] = curSteps + 1
            Q = append(Q, next)
        }
    }
    return steps
}
func readMaze(filename string) [][]int {
    file, err := os.Open(filename)
    if err != nil {
        panic(err)
    }
 
    var row, col int
    fmt.Fscanf(file, "%d %d", &row, &col)
 
    maze := make([][]int, row)
    for i := range maze {
        maze[i] = make([]int, col)
        for j := range maze[i] {
            fmt.Fscanf(file, "%d", &maze[i][j])
        }
    }
    return maze
}
 
func main() {
    maze := readMaze("maze/maze.in")
    /*for _, row := range maze {
        for _, val := range row {
            fmt.Printf("%d ", val)
        }
        fmt.Println()
    }*/
 
    steps:=walk(maze, point{0, 0}, point{len(maze) - 1, len(maze[0]) - 1})
    for _,row:=range steps {
        for _,val:=range row{
            fmt.Printf("%3d",val)
        }
        fmt.Println()
    }
}
结果:

总结

  • 用循环创建二维slice
  • 使用slice来实现队列
  • 用Fscanf读取文件
  • 对point的抽象

爬虫实战项目(十三)

本章将介绍项目的具体内容,课题的选择,技术选型,总体架构,以及实现步骤。

爬虫项目介绍

为什么做爬虫项目

  • 有一定的复杂性
  • 可以灵活调整项目的复杂性
  • 平衡语言/爬虫之间的比重

网络爬虫分类

  • 通用爬虫,如 baidu, google
  • 聚焦爬虫,从互联网获取结构化数据

go语言的爬虫库/框架

  • henrylee2cn/pholcus
  • gocrawl
  • colly
  • hu17889/go_spider

本课程爬虫项目

  • 将不使用现成爬虫库/框架
  • 使用 ElasticSearch 作为数据存储
  • 使用 Go 语言标准模板库实现 http 数据展示部分

爬虫的主题

爬取内容

  • 内容:如新闻,博客,社区…

爬取人

  • QQ 空间,人人网,微博,微信,facebook?
  • 相亲网站,求职网站
  • 出于隐私和趣味性考虑,本课程将爬取相亲网站

单任务版爬虫(十四)

在考虑性能之前我们首先应该考虑正确性。单任务版爬虫确保我们能够正确爬取我们所需的信息。我们应用了之前练习的广度优先算法,抽象出Parser和Fetcher,学习正则表达式,成功实现并运行单任务版爬虫。

获取城市名称和链接:

  • CSS选择器
    浏览器,console: $(’#cityList>dd>a’)
  • 使用xpath
  • 使用正则表达式

一.获得初始页面内容

gopm get -g -v golang.org/x/text  //引入gbk库
报错: bash: gopm: command not found
解决方法: 使用gopm 完成安装

gopm--Go Package Manager 的缩写。是go 上的包管理工具,十分好用。 gopm

1.gopm 安装:
这个十分简单只需一条命令就可以了:

go get -u github.com/gpmgo/gopm  //亲测可用
2.使用 gopm安装需要的包
gopm 具有丰富的包管理功能,具体的管理命令可以参考官方文档(官方文档有中文版 各位爽不爽)链接
这里只需要一条命令就可以搞定了:

gopm bin -d $GOPATH/bin PackageName

二.正则表达式获取邮件地址

package main
 
import (
    "fmt"
    "regexp"
)
 
const text = `
my email is lxw@qq.com
email2 is aa@def.com
email3 is bb@eft.com.cn
`
 
func main() {
    re := regexp.MustCompile(`([a-zA-Z0-9]+)@([a-zA-Z0-9]+)(\.[a-zA-Z0-9]+)`)
    match := re.FindAllStringSubmatch(text, -1)
    for _, m := range match {
        fmt.Println(m)
    }
}
2.提取城市和url

package main
 
import (
    "fmt"
    "io/ioutil"
    "net/http"
    "regexp"
)
 
func main() {
    resp, err := http.Get("http://www.zhenai.com/zhenghun")
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()
    if resp.StatusCode != http.StatusOK {
        fmt.Println("Error:status code", resp.StatusCode)
        return
    }
    all, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        panic(err)
    }
    //fmt.Printf("%s\n", all)
    printCityList(all)
}
 
func printCityList(contents []byte){
    re:=regexp.MustCompile(`<a href="(http://www.zhenai.com/zhenghun/[a-z0-9]+)"[^>]*>([^<]+)</a>`)
    match:=re.FindAllSubmatch(contents,-1)
    for _,m :=range match {
        //for _,sub:=range m {
        //    fmt.Printf("%s",sub)
        //}
        //fmt.Println()
        fmt.Printf("city: %s,  Url:%s \n",m[2],m[1])
    }
 
    fmt.Printf("matches found:%d\n",len(match))
}

regex.go

package main

import (

"fmt"

"regexp"

)

// const text = "My emails is 53550@qq.com"

const text = `

My emails is 53550@qq.com

email1 is abc@edf.org

email2 is kk@qq.com

email3 is ddd@abc.com.cn

`

func main() {

// re := regexp.MustCompile(`[a-zA-Z0-9]+@[a-zA-Z0-9.]+\.[a-zA-Z0-9]+`)

re := regexp.MustCompile(`([a-zA-Z0-9]+)@([a-zA-Z0-9]+)(\.[a-zA-Z0-9.]+)`)

// match := re.FindString(text)

// match := re.FindAllString(text, -1)

match := re.FindAllStringSubmatch(text, -1)

// fmt.Println(match)

for _, m := range match {

fmt.Println(m)

}

}

fetcher.go

package fetcher

import (

"bufio"

"fmt"

"io"

"io/ioutil"

"log"

"net/http"

"golang.org/x/net/html/charset"

"golang.org/x/text/encoding"

"golang.org/x/text/encoding/unicode"

"golang.org/x/text/transform"

)

func Fetch(url string) ([]byte, error) {

resp, err := http.Get(url)

if err != nil {

return nil, err

}

defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {

return nil,

fmt.Errorf("wrong status code: %d",

resp.StatusCode)

}

e := determineEncoding(resp.Body)

utf8Reader := transform.NewReader(resp.Body, e.NewDecoder())

return ioutil.ReadAll(utf8Reader)

}

func determineEncoding(r io.Reader) encoding.Encoding {

bytes, err := bufio.NewReader(r).Peek(1024)

if err != nil {

log.Printf("Fetcher error: %v", err)

return unicode.UTF8

}

e, _, _ := charset.DetermineEncoding(bytes, "")

return e

}

fetcher.go

package fetcher

import (

"bufio"

"fmt"

"io/ioutil"

"log"

"net/http"

"golang.org/x/net/html/charset"

"golang.org/x/text/encoding"

"golang.org/x/text/encoding/unicode"

"golang.org/x/text/transform"

)

func Fetch(url string) ([]byte, error) {

// resp, err := http.Get(url)

// if err != nil {

//  return nil, err

// }

// defer resp.Body.Close()

client := &http.Client{}

req, err := http.NewRequest("GET", url, nil)

if err != nil {

return nil, err

}

req.Header.Set("User-Agent", "xxxxMozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.75 Safari/537.36")

resp, err := client.Do(req)

if err != nil {

log.Fatalln(err)

}

defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {

return nil,

fmt.Errorf("wrong status code: %d",

resp.StatusCode)

}

bodyReader := bufio.NewReader(resp.Body)

e := determineEncoding(bodyReader)

utf8Reader := transform.NewReader(bodyReader, e.NewDecoder())

return ioutil.ReadAll(utf8Reader)

}

// func determineEncoding(r io.Reader) encoding.Encoding {

func determineEncoding(r *bufio.Reader) encoding.Encoding {

bytes, err := r.Peek(1024)

if err != nil {

log.Printf("Fetcher error: %v", err)

return unicode.UTF8

}

e, _, _ := charset.DetermineEncoding(bytes, "")

return e

}

parser

citylist.go

package parser

import (

"fmt"

"learngo/crawler/engine"

"regexp"

)

const cityListRe = `<a href="(http://www.zhenai.com/zhenghun/[a-z0-9]+)"[^>]*>([^<]+)</a>`

func ParserCityList(contents []byte) engine.ParseResult {

// re := regexp.MustCompile(`<a href="http://www.zhenai.com/zhenghun/[a-z0-9]+"[^>]*>[^<]+</a>`)

// re := regexp.MustCompile(`<a href="http://www.zhenai.com/zhenghun/aba" data-v-1573aa7c>阿坝</a>`)

// re := regexp.MustCompile(`<a href="(http://www.zhenai.com/zhenghun/[a-z0-9]+)"[^>]*>([^<]+)</a>`)

re := regexp.MustCompile(cityListRe)

// matches := re.FindAll(contents, -1)

// [][]byte

// for _, m := range matches {

//  fmt.Printf("%s\n", m)

// }

matches := re.FindAllSubmatch(contents, -1)

// [][][]byte

result := engine.ParseResult{}

for _, m := range matches {

// for _, subMatch := range m {

//  fmt.Printf("%s ", subMatch)

// }

// fmt.Println()

result.Items = append(

result.Items, string(m[2]))

result.Requests = append(

result.Requests, engine.Request{

Url:        string(m[1]),

ParserFunc: engine.NilParser,

})

fmt.Printf("city: %s,  Url:%s \n", m[1], m[0])

fmt.Println()

}

return result

}

citylist.go

package parser

import (

"learngo/crawler/engine"

"regexp"

)

const cityListRe = `<a href="(http://www.zhenai.com/zhenghun/[a-z0-9]+)"[^>]*>([^<]+)</a>`

func ParserCityList(contents []byte) engine.ParseResult {

// re := regexp.MustCompile(`<a href="http://www.zhenai.com/zhenghun/[a-z0-9]+"[^>]*>[^<]+</a>`)

// re := regexp.MustCompile(`<a href="http://www.zhenai.com/zhenghun/aba" data-v-1573aa7c>阿坝</a>`)

// re := regexp.MustCompile(`<a href="(http://www.zhenai.com/zhenghun/[a-z0-9]+)"[^>]*>([^<]+)</a>`)

re := regexp.MustCompile(cityListRe)

// matches := re.FindAll(contents, -1)

// [][]byte

// for _, m := range matches {

//  fmt.Printf("%s\n", m)

// }

matches := re.FindAllSubmatch(contents, -1)

// [][][]byte

result := engine.ParseResult{}

limit := 10

for _, m := range matches {

// for _, subMatch := range m {

//  fmt.Printf("%s ", subMatch)

// }

// fmt.Println()

result.Items = append(

// result.Items, string(m[2]))

result.Items, "City "+string(m[2]))

result.Requests = append(

result.Requests, engine.Request{

Url: string(m[1]),

// ParserFunc: engine.NilParser,

ParserFunc: ParserCity,

})

limit--

if limit == 0 {

break

}

// fmt.Printf("city: %s,  Url:%s \n", m[2], m[1])

// fmt.Println()

}

return result

}

citylist_test.go

package parser

import (

"io/ioutil"

"testing"

)

func TestParseCityList(t *testing.T) {

contents, err := ioutil.ReadFile("citylist_test_data.html")

if err != nil {

panic(err)

}

// fmt.Printf("%s\n", contents)

//verify result

result := ParserCityList(contents)

const resultSize = 470

expectedUrls := []string{

"http://www.zhenai.com/zhenghun/aba", "http://www.zhenai.com/zhenghun/akesu", "http://www.zhenai.com/zhenghun/alashanmeng",

}

expectedCities := []string{

"City 阿坝", "City 阿克苏", "City 阿拉善盟",

}

if len(result.Requests) != resultSize {

t.Errorf("result should have %d"+

"requests; but had %d",

resultSize, len(result.Requests))

}

for i, url := range expectedUrls {

if result.Requests[i].Url != url {

t.Errorf("expected url #%d: %s; but"+

"was %s",

i, url, result.Requests[i].Url)

}

}

if len(result.Items) != resultSize {

t.Errorf("result should have %d"+

"requests; but had %d",

resultSize, len(result.Items))

}

for i, city := range expectedCities {

if result.Items[i].(string) != city {

t.Errorf("expected city #%d: %s; but"+

"was %s",

i, city, result.Items[i].(string))

}

}

}

city.go

package parser

import (

"learngo/crawler/engine"

"regexp"

)

// const cityRe = `<a href="http://album.zhenai.com/u/1573987492" target="_blank">心随灵动</a>`

const cityRe = `<a href="(http://album.zhenai.com/u/[0-9]+)"[^>]*>([^<]+)</a>`

func ParserCity(contents []byte) engine.ParseResult {

// re := regexp.MustCompile(`<a href="http://www.zhenai.com/zhenghun/[a-z0-9]+"[^>]*>[^<]+</a>`)

// re := regexp.MustCompile(`<a href="http://www.zhenai.com/zhenghun/aba" data-v-1573aa7c>阿坝</a>`)

// re := regexp.MustCompile(`<a href="(http://www.zhenai.com/zhenghun/[a-z0-9]+)"[^>]*>([^<]+)</a>`)

re := regexp.MustCompile(cityRe)

// matches := re.FindAll(contents, -1)

// [][]byte

// for _, m := range matches {

//  fmt.Printf("%s\n", m)

// }

matches := re.FindAllSubmatch(contents, -1)

// [][][]byte

result := engine.ParseResult{}

for _, m := range matches {

// for _, subMatch := range m {

//  fmt.Printf("%s ", subMatch)

// }

// fmt.Println()

name := string(m[2])

result.Items = append(

result.Items, "User "+name)

result.Requests = append(

result.Requests, engine.Request{

Url: string(m[1]),

// ParserFunc: engine.NilParser,

// ParserFunc: ParseProfile,

ParserFunc: func(c []byte) engine.ParseResult {

return ParseProfile(c, name)

},

})

}

return result

}

profile.go

package parser

import (

"learngo/crawler/engine"

"learngo/crawler/model"

"regexp"

"strconv"

)

var ageRe = regexp.MustCompile(`<td><span class="label">年龄:</span>(\d+)岁</td>`)

var heightRe = regexp.MustCompile(`<td><span class="label">身高:</span>(\d+)CM</td>`)

var incomeRe = regexp.MustCompile(`<td><span class="label">月收入:</span>([^<]+)</td>`)

var weightRe = regexp.MustCompile(`<td><span class="label">体重:</span><span field="">([^<]+)</td>`)

var genderRe = regexp.MustCompile(`<td><span class="label">性别:</span><span field="">([^<]+)</td>`)

var xinzuoRe = regexp.MustCompile(`<td><span class="label">星座:</span><span field="">([^<]+)</td>`)

var marriageRe = regexp.MustCompile(`<td><span class="label">婚况:</span>(^<+)</td>`)

var educationRe = regexp.MustCompile(`<td><span class="label">学历:</span>(^<+)</td>`)

var occupationRe = regexp.MustCompile(`<td><span class="label">职业:</span><span field="">([^<]+)</td>`)

var hukouRe = regexp.MustCompile(`<td><span class="label">籍贯:</span>([^<]+)</td>`)

var houseRe = regexp.MustCompile(`<td><span class="label">住房条件:</span><span field="">([^<]+)</td>`)

var carRe = regexp.MustCompile(`<td><span class="label">是否购车:</span><span field="">([^<]+)</td>`)

// func ParseProfile(contents []byte) engine.ParseResult {

func ParseProfile(contents []byte, name string) engine.ParseResult {

profile := model.Profile{}

profile.Name = name

age, err := strconv.Atoi(extractString(contents, ageRe))

if err != nil {

profile.Age = age

}

height, err := strconv.Atoi(extractString(contents, heightRe))

if err != nil {

profile.Height = height

}

weight, err := strconv.Atoi(extractString(contents, weightRe))

if err != nil {

profile.Weight = weight

}

profile.Income = extractString(contents, incomeRe)

profile.Gender = extractString(contents, genderRe)

profile.Car = extractString(contents, carRe)

profile.Education = extractString(contents, educationRe)

profile.Hukou = extractString(contents, hukouRe)

profile.House = extractString(contents, houseRe)

profile.Marriage = extractString(contents, marriageRe)

profile.Occupation = extractString(contents, occupationRe)

profile.Xinzuo = extractString(contents, xinzuoRe)

result := engine.ParseResult{

Items: []interface{}{profile},

}

return result

}

func extractString(contents []byte, re *regexp.Regexp) string {

match := re.FindSubmatch(contents)

if len(match) >= 2 {

return string(match[1])

} else {

return ""

}

}

profile_test.go

package parser

import (

"io/ioutil"

"learngo/crawler/model"

"testing"

)

func TestParseProfile(t *testing.T) {

contents, err := ioutil.ReadFile("profile_test_data.html")

if err != nil {

panic(err)

}

// fmt.Printf("%s\n", contents)

//verify result

result := ParseProfile(contents, "安静的雪")

if len(result.Items) != 1 {

t.Errorf("Items should contain 1"+

"element; but was %v",

result.Items)

}

profile := result.Items[0].(model.Profile)

expected := model.Profile{

Age:        34,

Height:     162,

Weight:     57,

Income:     "3001-5000元",

Gender:     "女",

Name:       "安静的雪",

Xinzuo:     "牡羊座",

Occupation: "人事/行政",

Marriage:   "离异",

House:      "已购房",

Hukou:      "山东菏泽",

Education:  "大学本科",

}

if profile != expected {

t.Errorf("expected %v: but was %v",

expected, profile)

}

}

engine

type.go

package engine

type Request struct {

Url        string

ParserFunc func([]byte) ParseResult

// ParserFunc func([]byte, map[string]string) ParseResult

}

type ParseResult struct {

Requests []Request

Items    []interface{}

}

func NilParser([]byte) ParseResult {

return ParseResult{}

}

engine.go

package engine

import (

"learngo/crawler/fetcher"

"log"

)

func Run(seeds ...Request) {

var requests []Request

for _, r := range seeds {

requests = append(requests, r)

}

for len(requests) > 0 {

r := requests[0]

requests = requests[1:]

log.Printf("Fetching %s", r.Url)

body, err := fetcher.Fetch(r.Url)

if err != nil {

log.Printf("Fetcher: err fetching url %s: %v", r.Url, err)

continue

}

parseResult := r.ParserFunc(body)

requests = append(requests, parseResult.Requests...)

for _, item := range parseResult.Items {

log.Printf("Got item %v", item)

}

}

}

main.go

package main

import (

"learngo/crawler/engine"

"learngo/crawler/zhenai/parser"

)

func main() {

engine.Run(engine.Request{

Url:        "http://www.zhenai.com/zhenghun",

ParserFunc: parser.ParserCityList,

})

}

解决方案:模仿浏览器访问。

什么是User-agent?
User Agent中文名为用户代理,简称 UA,它是一个特殊字符串头,使得服务器能够识别客户使用的操作系统及版本、CPU 类型、浏览器及版本、浏览器渲染引擎、浏览器语言、浏览器插件等。

常常要用server抓资料时,都会碰到直接使用wget和curl被服务器拒绝的状况。通常简单加个user-agent伪装一下就会过了。

2.查看自己浏览器中的User-Agent 信息。
进入浏览器,我用的是Google chrome浏览器,按住F12,或者单击右键点击检查

3.在Network中,点击ALL,如果没有结果,按ctrl+R,之后找到对应Name下的网站名,在Headers中最后可以看到user-agent

User-Agent:

Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.75 Safari/537.36

4.将以下代码替换:

resp, err := http.Get(url)

if err != nil {

return nil, err

}

defer resp.Body.Close()

替换成:

client := &http.Client{}
    req, err := http.NewRequest("GET", url, nil)
    if err != nil {
        return nil, err
    }
    req.Header.Set("User-Agent", "XXXX")//刚才找到的浏览器中User-agent

resp, err := client.Do(req)
    if err != nil {
        log.Fatalln(err)
    }

defer resp.Body.Close()

单任务版爬虫性能

获取并打印所有城市第一页用户的详细信息

2. wrong status code:202
主要原因是服务器反爬虫机制导致,简单来说就是发给服务器的请求cookie验证不通过。完全解决稍微复杂,若想简单通过调试可采用如下方法:

  • 将得到的url中的http转成https

newUrl := strings.Replace(url, "http://", "https://", 1)

去浏览器的请求中找一个cookie(只有1分钟有效期)

cookie1 := "xxx"// 上图找到的

cookie req.Header.Add("cookie", cookie1)

不用引额外的包即可简单解决。

完整代码片段
将其替换fetcher部分可临时解决供调试用,不影响代码理解

client := &http.Client{}
    newUrl := strings.Replace(url, "http://", "https://", 1)
    req, err := http.NewRequest("GET", newUrl, nil)
    if err != nil {
        panic(err)
        return nil, err
    }
    req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36") User-agent
    cookie1 := "xxx"
    req.Header.Add("cookie", cookie1)
    resp, err := client.Do(req)
    if err != nil {
        log.Fatalln(err)
    }
    defer resp.Body.Close()
    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("wrong status code: %d", resp.StatusCode)
    }

并发版爬虫(十五)

为了提升爬虫性能,我们抽象出Worker的概念,并添加调度器,实现并发版爬虫。我们应用接口的概念,完成了由简至复杂的多个调度器的实现。同学可以在实战项目中更真实的体会并学习Go语言并发编程的多种模式。

engine

concurrent.go

package engine

import (

"log"

)

type ConcurrentEngine struct {

Scheduler   Scheduler

WorkerCount int

}

type Scheduler interface {

ReadyNotifier

Submit(Request)

// ConfigureMasterWorkerChan(chan Request)

WorkerChan() chan Request

Run()

}

type ReadyNotifier interface {

WorkerReady(chan Request)

}

func (e *ConcurrentEngine) Run(seeds ...Request) {

// in := make(chan Request)

out := make(chan ParseResult)

// e.Scheduler.ConfigureMasterWorkerChan(in)

e.Scheduler.Run()

for i := 0; i < e.WorkerCount; i++ {

// createWorker(in, out)

// createWorker(out, e.Scheduler)

// createWorker(e.Scheduler.WorkerChan(), out, e.Scheduler)

e.createWorker(e.Scheduler.WorkerChan(), out, e.Scheduler)

}

for _, r := range seeds {

e.Scheduler.Submit(r)

}

itemCount := 0

for {

result := <-out

for _, item := range result.Items {

log.Printf("Got item #%d: %v", itemCount, item)

itemCount++

}

for _, request := range result.Requests {

e.Scheduler.Submit(request)

}

}

}

// func createWorker(in chan Request, out chan ParseResult){

// func createWorker(out chan ParseResult, s Scheduler) {

// func createWorker(in chan Request, out chan ParseResult, s Scheduler) {

func (e *ConcurrentEngine) createWorker(in chan Request, out chan ParseResult, ready ReadyNotifier) {

// in := make(chan Request)

go func() {

for {

// tell scheduler i'm ready

// s.WorkerReady(in)

ready.WorkerReady(in)

request := <-in

result, err := Worker(request)

if err != nil {

continue

}

out <- result

}

}()

}

simple.go

package engine

import (

"learngo/crawler/fetcher"

"log"

)

type SimpleEngine struct{}

func (e SimpleEngine) Run(seeds ...Request) {

var requests []Request

for _, r := range seeds {

requests = append(requests, r)

}

for len(requests) > 0 {

r := requests[0]

requests = requests[1:]

log.Printf("Fetching %s", r.Url)

parseResult, err := Worker(r)

if err != nil {

continue

}

requests = append(requests, parseResult.Requests...)

for _, item := range parseResult.Items {

log.Printf("Got item %v", item)

}

}

}

func Worker(r Request) (ParseResult, error) {

log.Printf("Fetching %s", r.Url)

body, err := fetcher.Fetch(r.Url)

if err != nil {

log.Printf("Fetcher: err fetching url %s: %v", r.Url, err)

return ParseResult{}, err

}

return r.ParserFunc(body), nil

}

types.go

package engine

type Request struct {

Url        string

ParserFunc func([]byte) ParseResult

// ParserFunc func([]byte, map[string]string) ParseResult

}

type ParseResult struct {

Requests []Request

Items    []interface{}

}

func NilParser([]byte) ParseResult {

return ParseResult{}

}

fetcher

fetcher.go

package fetcher

import (

"bufio"

"fmt"

"io/ioutil"

"log"

"net/http"

"time"

"golang.org/x/net/html/charset"

"golang.org/x/text/encoding"

"golang.org/x/text/encoding/unicode"

"golang.org/x/text/transform"

)

var rateLimiter = time.Tick(10 * time.Millisecond)

func Fetch(url string) ([]byte, error) {

// resp, err := http.Get(url)

// if err != nil {

//  return nil, err

// }

// defer resp.Body.Close()

<-rateLimiter

client := &http.Client{}

req, err := http.NewRequest("GET", url, nil)

if err != nil {

return nil, err

}

req.Header.Set("User-Agent", "xxxxMozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.75 Safari/537.36")

resp, err := client.Do(req)

if err != nil {

log.Fatalln(err)

}

defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {

return nil,

fmt.Errorf("wrong status code: %d",

resp.StatusCode)

}

//不要peek

bodyReader := bufio.NewReader(resp.Body)

//自动find encoding

e := determineEncoding(bodyReader)

utf8Reader := transform.NewReader(bodyReader, e.NewDecoder())

return ioutil.ReadAll(utf8Reader)

}

// func determineEncoding(r io.Reader) encoding.Encoding {

func determineEncoding(r *bufio.Reader) encoding.Encoding {

// bytes, err := r.Peek(1024)

//bytes, err := bufio.NewReader(r).Peek(1024)

bytes, err := bufio.NewReader(r).Peek(1024)

if err != nil {

log.Printf("Fetcher error: %v", err)

return unicode.UTF8

}

e, _, _ := charset.DetermineEncoding(bytes, "")

return e

}

model

profile.go

package model

import "encoding/json"

type Profile struct {

Name       string

Gender     string

Age        int

Height     int

Weight     int

Income     string

Marriage   string

Education  string

Occupation string

Hukou      string

Xinzuo     string

House      string

Car        string

}

func FromJsonObj(o interface{}) (Profile, error) {

var profile Profile

s, err := json.Marshal(o)

if err != nil {

return profile, err

}

json.Unmarshal(s, &profile)

return profile, err

}

scheduler.go

package model

import "encoding/json"

type Profile struct {

Name       string

Gender     string

Age        int

Height     int

Weight     int

Income     string

Marriage   string

Education  string

Occupation string

Hukou      string

Xinzuo     string

House      string

Car        string

}

/*

func FromJsonObj(o interface{}) (Profile, error) {

var profile Profile

s, err := json.Marshal(o)

if err != nil {

return profile, err

}

json.Unmarshal(s, &profile)

return profile, err

}

*/

scheduler

queued.go

package scheduler

import "learngo/crawler/engine"

type QueuedScheduler struct {

requestChan chan engine.Request

workerChan  chan chan engine.Request

}

func (s *QueuedScheduler) WorkerChan() chan engine.Request {

return make(chan engine.Request)

}

func (s *QueuedScheduler) Submit(r engine.Request) {

s.requestChan <- r

}

func (s *QueuedScheduler) WorkerReady(w chan engine.Request) {

s.workerChan <- w

}

// func (s *QueuedScheduler) ConfigureMasterWorkerChan(chan engine.Request) {

// }

func (s *QueuedScheduler) Run() {

s.workerChan = make(chan chan engine.Request)

s.requestChan = make(chan engine.Request)

go func() {

var requestQ []engine.Request

var workerQ []chan engine.Request

for {

var activeRequest engine.Request

var activeWorker chan engine.Request

if len(requestQ) > 0 &&

len(workerQ) > 0 {

activeWorker = workerQ[0]

activeRequest = requestQ[0]

}

select {

case r := <-s.requestChan:

// send r to a ?worker

requestQ = append(requestQ, r)

case w := <-s.workerChan:

// send ?next_request to a w

workerQ = append(workerQ, w)

case activeWorker <- activeRequest:

workerQ = workerQ[1:]

requestQ = requestQ[1:]

}

}

}()

}

simple.go

package scheduler

import "learngo/crawler/engine"

type SimpleScheduler struct {

workerChan chan engine.Request

}

func (s *SimpleScheduler) WorkerChan() chan engine.Request {

return s.workerChan

}

func (s *SimpleScheduler) WorkerReady(chan engine.Request) {

}

func (s *SimpleScheduler) Run() {

s.workerChan = make(chan engine.Request)

}

// func (s *SimpleScheduler) ConfigureMasterWorkerChan(c chan engine.Request) {

//  s.workerChan = c

// }

func (s *SimpleScheduler) Submit(r engine.Request) {

//send request down to worker chan

go func() { s.workerChan <- r }()

}

zhenai/parser

city.go

package parser

import (

"learngo/crawler/engine"

"regexp"

)

// const cityRe = `<a href="http://album.zhenai.com/u/1573987492" target="_blank">心随灵动</a>`

// const cityRe = `<a href="(http://album.zhenai.com/u/[0-9]+)"[^>]*>([^<]+)</a>`

var (

profileRe = regexp.MustCompile(`<a href="(http://album.zhenai.com/u/[0-9]+)"[^>]*>([^<]+)</a>`)

cityUrlRe = regexp.MustCompile(`href="(http://www.zhenai.com/zhenghun/[^"]+)"`)

)

func ParseCity(contents []byte) engine.ParseResult {

// func ParseCity(contents []byte, _ string) engine.ParseResult {

// re := regexp.MustCompile(`<a href="http://www.zhenai.com/zhenghun/[a-z0-9]+"[^>]*>[^<]+</a>`)

// re := regexp.MustCompile(`<a href="http://www.zhenai.com/zhenghun/aba" data-v-1573aa7c>阿坝</a>`)

// re := regexp.MustCompile(`<a href="(http://www.zhenai.com/zhenghun/[a-z0-9]+)"[^>]*>([^<]+)</a>`)

// re := regexp.MustCompile(cityRe)

// matches := re.FindAll(contents, -1)

// [][]byte

// for _, m := range matches {

//  fmt.Printf("%s\n", m)

// }

// matches := re.FindAllSubmatch(contents, -1)

matches := profileRe.FindAllSubmatch(contents, -1)

// [][][]byte

result := engine.ParseResult{}

for _, m := range matches {

// for _, subMatch := range m {

//  fmt.Printf("%s ", subMatch)

// }

// fmt.Println()

url := string(m[1])

name := string(m[2])

// result.Items = append(

//  result.Items, "User "+name)

result.Requests = append(

result.Requests, engine.Request{

// Url: string(m[1]),

Url: url,

// ParserFunc: engine.NilParser,

// ParserFunc: ParseProfile,

ParserFunc: func(c []byte) engine.ParseResult {

return ParseProfile(c, name)

},

})

}

matches = cityUrlRe.FindAllSubmatch(contents, -1)

for _, m := range matches {

result.Requests = append(result.Requests,

engine.Request{

Url:        string(m[1]),

ParserFunc: ParseCity,

})

}

return result

}

citylist_test.go

citylist.go

package parser

import (

"learngo/crawler/engine"

"regexp"

)

const cityListRe = `<a href="(http://www.zhenai.com/zhenghun/[a-z0-9]+)"[^>]*>([^<]+)</a>`

func ParserCityList(contents []byte) engine.ParseResult {

// re := regexp.MustCompile(`<a href="http://www.zhenai.com/zhenghun/[a-z0-9]+"[^>]*>[^<]+</a>`)

// re := regexp.MustCompile(`<a href="http://www.zhenai.com/zhenghun/aba" data-v-1573aa7c>阿坝</a>`)

// re := regexp.MustCompile(`<a href="(http://www.zhenai.com/zhenghun/[a-z0-9]+)"[^>]*>([^<]+)</a>`)

re := regexp.MustCompile(cityListRe)

// matches := re.FindAll(contents, -1)

// [][]byte

// for _, m := range matches {

//  fmt.Printf("%s\n", m)

// }

matches := re.FindAllSubmatch(contents, -1)

// [][][]byte

result := engine.ParseResult{}

// limit := 10

for _, m := range matches {

// for _, subMatch := range m {

//  fmt.Printf("%s ", subMatch)

// }

// fmt.Println()

result.Items = append(

// result.Items, string(m[2]))

result.Items, "City "+string(m[2]))

result.Requests = append(

result.Requests, engine.Request{

Url: string(m[1]),

// ParserFunc: engine.NilParser,

ParserFunc: ParseCity,

})

// limit--

// if limit == 0 {

//  break

// }

// fmt.Printf("city: %s,  Url:%s \n", m[2], m[1])

// fmt.Println()

}

return result

}

profile_test.go

profile.go

package parser

import (

"learngo/crawler/engine"

"learngo/crawler/model"

"regexp"

"strconv"

)

var ageRe = regexp.MustCompile(`<td><span class="label">年龄:</span>(\d+)岁</td>`)

var heightRe = regexp.MustCompile(`<td><span class="label">身高:</span>(\d+)CM</td>`)

var incomeRe = regexp.MustCompile(`<td><span class="label">月收入:</span>([^<]+)</td>`)

var weightRe = regexp.MustCompile(`<td><span class="label">体重:</span><span field="">([^<]+)</td>`)

var genderRe = regexp.MustCompile(`<td><span class="label">性别:</span><span field="">([^<]+)</td>`)

var xinzuoRe = regexp.MustCompile(`<td><span class="label">星座:</span><span field="">([^<]+)</td>`)

var marriageRe = regexp.MustCompile(`<td><span class="label">婚况:</span>(^<+)</td>`)

var educationRe = regexp.MustCompile(`<td><span class="label">学历:</span>(^<+)</td>`)

var occupationRe = regexp.MustCompile(`<td><span class="label">职业:</span><span field="">([^<]+)</td>`)

var hukouRe = regexp.MustCompile(`<td><span class="label">籍贯:</span>([^<]+)</td>`)

var houseRe = regexp.MustCompile(`<td><span class="label">住房条件:</span><span field="">([^<]+)</td>`)

var carRe = regexp.MustCompile(`<td><span class="label">是否购车:</span><span field="">([^<]+)</td>`)

// const ageRe = `<td><span class="label">年龄:</span>(\d+)岁</td>`

// var ageRex = regexp.MustCompile(ageRe)

// var heightRe = regexp.MustCompile(`<td><span class="label">身高:</span>(\d+)CM</td>`)

// var incomeRe = regexp.MustCompile(`<td><span class="label">月收入:</span>([^>]+)</td>`)

// var weightRe = regexp.MustCompile(`<td><span class="label">体重:</span><span field="">(\d+)KG</span></td>`)

// var genderRe = regexp.MustCompile(`<td><span class="label">性别:</span><span field="">([^>]+)</span></td>`)

// var xinzuoRe = regexp.MustCompile(`<td><span class="label">星座:</span><span field="">([^>]+)</span></td>`)

// const marriageRe = `<td><span class="label">婚况:</span>([^>]+)</td>`

// var marriageRex = regexp.MustCompile(marriageRe)

// var educationRe = regexp.MustCompile(`<td><span class="label">学历:</span>([^>]+)</td>`)

// var OccupationRe = regexp.MustCompile(`<td><span class="label">职业: </span>([^>]+)</td>`)

// var HukouRe = regexp.MustCompile(`<td><span class="label">籍贯:</span>([^>]+)</td>`)

// var HouseRe = regexp.MustCompile(`<td><span class="label">住房条件:</span><span field="">([^>]+)</span></td>`)

// var CarRe = regexp.MustCompile(`<td><span class="label">是否购车:</span><span field="">([^>]+)</span></td>`)

// //猜你喜欢的,提取url, name

// var guessRe = regexp.MustCompile(`<a class="exp-user-name"[^>]*href="(http://album.zhenai.com/u/[\d]+)">([^<])`)

// //提取id

// var idUrlRe = regexp.MustCompile(`http://album.zhenai.com/u/([\d]+)`)

// func ParseProfile(contents []byte) engine.ParseResult {

// func ParseProfile(contents []byte, name string) engine.ParseResult {

func ParseProfile(contents []byte, name string) engine.ParseResult {

profile := model.Profile{}

profile.Name = name

age, err := strconv.Atoi(extractString(contents, ageRe))

if err != nil {

profile.Age = age

}

height, err := strconv.Atoi(extractString(contents, heightRe))

if err != nil {

profile.Height = height

}

weight, err := strconv.Atoi(extractString(contents, weightRe))

if err != nil {

profile.Weight = weight

}

profile.Income = extractString(contents, incomeRe)

profile.Gender = extractString(contents, genderRe)

profile.Car = extractString(contents, carRe)

profile.Education = extractString(contents, educationRe)

profile.Hukou = extractString(contents, hukouRe)

profile.House = extractString(contents, houseRe)

profile.Marriage = extractString(contents, marriageRe)

profile.Occupation = extractString(contents, occupationRe)

profile.Xinzuo = extractString(contents, xinzuoRe)

result := engine.ParseResult{

Items: []interface{}{profile},

// Items: []engine.Item{

//  {

//      Url:     url,

//      Type:    "zhenai",

//      Id:      extractString([]byte(url), idUrlRe),

//      Payload: profile,

//  },

// },

}

//猜你喜欢的人

// matches := guessRe.FindAllSubmatch(contents, -1)

// for _, m := range matches {

//  url := string(m[1])

//  name := string(m[2])

//  result.Requests = append(result.Requests,

//      engine.Request{

//          Url:        url,

//          ParserFunc: ProfileParser(name),

//          // Parser:NewProfileParser(name),

//      })

// }

return result

}

func extractString(contents []byte, re *regexp.Regexp) string {

match := re.FindSubmatch(contents)

if len(match) >= 2 {

return string(match[1])

} else {

return ""

}

}

// type ProfileParser struct {

//  userName string

// }

// func ProfileParser(name string) engine.ParserFunc {

//  return func(c []byte, url string) engine.ParseResult {

//      return ParseProfile(c, url, name)

//  }

// }

main.go

package main

import (

"learngo/crawler/engine"

"learngo/crawler/scheduler"

"learngo/crawler/zhenai/parser"

)

func main() {

// engine.SimpleEngine{}.Run(engine.Request{

//  Url:        "http://www.zhenai.com/zhenghun",

//  ParserFunc: parser.ParserCityList,

//  })

e := engine.ConcurrentEngine{

// Scheduler:   &scheduler.SimpleScheduler{},

Scheduler:   &scheduler.QueuedScheduler{},

WorkerCount: 100,

}

// e.Run(engine.Request{

//  //种子 Url

//  Url:        "http://www.zhenai.com/zhenghun",

//  ParserFunc: parser.ParserCityList,

// })

e.Run(engine.Request{

//种子 Url

Url:        "http://www.zhenai.com/zhenghun/shanghai",

ParserFunc: parser.ParseCity,

})

}

数据存储和展示(十六)

是时候检验我们项目的成果了。我们将采用Docker+ElasticSearch来存储我们爬取的信息。在简单了解Docker和ElasticSearch后,我们将使用ElasticSearch的Go语言客户端将爬取数据写入。之后我们使用Go语言的模板引擎迅速实现前端网页展示。至此,我们已经可以尝试自己喜欢的搜索条件去查看数据啦。

6.2 URL 去重

  • 哈希表
  • 计算MD5等哈希,再存哈希表
  • 使用bloom filter 多重哈希结果
  • 使用Redis等key-value存储系统实现分布式去重

ElasticSearch

作用

  • 存储我们爬取的数据
  • 不需要建表,配置字段等
  • json 格式的文档
  • 寻找:男,有房,有车
  • 寻找:女,年轻漂亮 -> 身材好 -> 年前身材指标好
  • 原生支持,不需要写代码,不需要拼装查询语句

安装

  • 方法一:从网站下载安装包,安装,配置,运行
  • 方法二:直接使用docker部署

docker安装ElasticSearch

  • 官网地址:https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html

ElasticSearch 初识

  • 使用REST接口
  • :9200/index/type/id
  • index -> database
  • type -> table
  • type 中的数据类型可以不一致
  • 可以使用_mapping来配置类型 //_mapping
  • 不需要预先创建index和type
  • POST/PUT 创建/修改数据,使用POST可省略id
  • GET获取数据
  • GET //_search?q=全文搜索

html/template 展示数据

  • 模板引擎
  • 服务器端生成最终网页
  • 适合做后台或者维护页面
  • 功能
    • 取值
    • 选择
    • 循环
    • 函数调用

分布式爬虫(十七)

本章在简要介绍分布式概念后,将我们的并发爬虫改写成分布式。我们在很少改动的情况下,加入jsonrpc客户/服务端,实现并部署分布式爬虫。最后探讨实战项目的更多改进方案。

分布式系统

1. 多个节点

  • 容错性
  • 可扩展性(性能)
  • 固有分布性

2. 消息传递

  • 节点具有私有存储

  • 易于开发

  • 可扩展性(功能)

  • 对比:并行计算

  • 消息传递方法

    • REST 接口
    • RPC
    • 中间件

  • 一般消息传递的方法

    • 对外: REST
    • 模块内部: RPC
    • 模块之间: 中间件, REST

3. 完成特定需求

分布式架构 vs 微服务架构

  • 分布式:指导节点之间如何通信
  • 微服务:鼓励按业务划分模块
  • 微服务架构通过分布式架构来实现

多层架构 vs 微服务架构

  • 微服务架构具有更多的 "服务"
  • 微服务通常需要配合自动化测试,部署,服务发现等

并发版的问题

1. 限流问题

  • 单节点能够承受的流量有限
  • 将Worker放到不同的节点

2.去重问题

  • 单节点能够承受的去重数据量有限
  • 无法保存之前去重结果
  • 基于Key-Value Store (如Redis) 进行分布式去重

3. 数据存储问题

  • 存储部分的结构,技术栈和爬虫部分区别很大
  • 进一步优化需要特殊的ElasticSearch技术背景
  • 固有分布式

本课程架构

  • 解决限流问题(理论上)
  • 解决存储问题
  • 分布式去重

docker run redis

docker run redis-go-client

将原本存在map中的数据存到redis中,就行了。

RPC

  • jsonrpc(本课程采用) demo代码:chapter_18/rpc
  • grpc(使用protobuf)
  • Thrift

自有协议

 

  • docker/libchan
  • NATS streaming
  • gociruit
  • 根据自己需求

解析器的序列化/反序列化

  • 解析器原先的定义为函数
  • 需要处理函数的序列化/反序列化 

总结

课程总结(十八)

那些go语言没有的元素

1. 类,继承,多态,重载

  • go语言拥有不同的世界观
  • 在面向对象界,也流行变继承为组合的思维
  • 这些面向对象的元素太容易被滥用
  • go语言为组合提供了更便捷的支持

2. try/catch/finally

  • 太多错误被当做异常
  • 很多c++项目组本身禁用try/catch
  • 正确的使用try/catch处理错误,导致代码混乱
  • try/catch在产品代码中并不能减小开发人员的负担
  • go也有defer/panic/recover 模式

3. 泛型

  • 泛型作为模板类型

    • 这种泛型实际想实现duck typing
    • go语言提供了对 duck typing, 和接口组合的支持
  • 泛型约束参数类型

    // 伪代码
    List<Integer>.add("abc") // compile error
    List<String>.add("abc") // OK
    • 这种做法本身非常复杂:类型通配符,covariance等问题
    • go语言本身自带强类型的slice,map,channel
    • 使用type assertion 甚至 go generate 来实现自己的泛型
    • 泛型支持是go作者唯一态度不强硬的点

4. 构造函数/解析函数/RAII c++

  • 大型项目中很少使用构造函数,多使用工厂函数
  • 值类型的构造由结构体初始化语法实现
  • RAII 技巧性太强,隐藏了意图
  • 析构函数与垃圾回收不匹配

5. 操作符重载

6. assert

课程总结

变量

选择,循环

指针,数组,容器

面向接口

结构体

duck typing的概念

组合的思想

函数式编程

闭包的概念

多样的例题

工程化

资源管理,错误处理

测试和文档

性能调优

并发编程

goroutine和channel

理解调度器

多样的例题

标准库

fmt,log

errors

io,bufio

time

net/http,net/rpc

html/template

charset,encoding,unicode,utf8

strings,bytes,strconv

regexp

flag

math

os

pprof

runtime

reflect

testing

实战项目

从0开始,使用Go语言自主搭建简单分布式爬虫

《Google资深工程师深度讲解Go语言》学习笔记相关推荐

  1. 第二行代码学习笔记——第六章:数据储存全方案——详解持久化技术

    本章要点 任何一个应用程序,总是不停的和数据打交道. 瞬时数据:指储存在内存当中,有可能因为程序关闭或其他原因导致内存被回收而丢失的数据. 数据持久化技术,为了解决关键性数据的丢失. 6.1 持久化技 ...

  2. 第一行代码学习笔记第二章——探究活动

    知识点目录 2.1 活动是什么 2.2 活动的基本用法 2.2.1 手动创建活动 2.2.2 创建和加载布局 2.2.3 在AndroidManifest文件中注册 2.2.4 在活动中使用Toast ...

  3. 第一行代码学习笔记第八章——运用手机多媒体

    知识点目录 8.1 将程序运行到手机上 8.2 使用通知 * 8.2.1 通知的基本使用 * 8.2.2 通知的进阶技巧 * 8.2.3 通知的高级功能 8.3 调用摄像头和相册 * 8.3.1 调用 ...

  4. 第一行代码学习笔记第六章——详解持久化技术

    知识点目录 6.1 持久化技术简介 6.2 文件存储 * 6.2.1 将数据存储到文件中 * 6.2.2 从文件中读取数据 6.3 SharedPreferences存储 * 6.3.1 将数据存储到 ...

  5. 第一行代码学习笔记第三章——UI开发的点点滴滴

    知识点目录 3.1 如何编写程序界面 3.2 常用控件的使用方法 * 3.2.1 TextView * 3.2.2 Button * 3.2.3 EditText * 3.2.4 ImageView ...

  6. 第一行代码学习笔记第十章——探究服务

    知识点目录 10.1 服务是什么 10.2 Android多线程编程 * 10.2.1 线程的基本用法 * 10.2.2 在子线程中更新UI * 10.2.3 解析异步消息处理机制 * 10.2.4 ...

  7. 第一行代码学习笔记第七章——探究内容提供器

    知识点目录 7.1 内容提供器简介 7.2 运行权限 * 7.2.1 Android权限机制详解 * 7.2.2 在程序运行时申请权限 7.3 访问其他程序中的数据 * 7.3.1 ContentRe ...

  8. 第一行代码学习笔记第五章——详解广播机制

    知识点目录 5.1 广播机制 5.2 接收系统广播 * 5.2.1 动态注册监听网络变化 * 5.2.2 静态注册实现开机广播 5.3 发送自定义广播 * 5.3.1 发送标准广播 * 5.3.2 发 ...

  9. 第一行代码学习笔记第九章——使用网络技术

    知识点目录 9.1 WebView的用法 9.2 使用HTTP协议访问网络 * 9.2.1 使用HttpURLConnection * 9.2.2 使用OkHttp 9.3 解析XML格式数据 * 9 ...

  10. 安卓教程----第一行代码学习笔记

    安卓概述 系统架构 Linux内核层,还包括各种底层驱动,如相机驱动.电源驱动等 系统运行库层,包含一些c/c++的库,如浏览器内核webkit.SQLlite.3D绘图openGL.用于java运行 ...

最新文章

  1. TypeError: unhashable type: 'dict'
  2. php json 转 xml格式,PHP中如何将JSON文件转XML格式
  3. qt for 3520a
  4. Java黑皮书课后题第7章:7.26(完全相同的数组)如果两个数组list1和list2的对应元素都相等,认为完全相同。编写一个测试程序,提示用户输入两个整数列表,然后显示这两个列表是否完全相同
  5. Java if语句深度解析
  6. 联想拯救者y7000加内存条_关于2020款联想拯救者Y7000、R7000和Y7000P,r7000p选哪个好?看这里就对了...
  7. 安杰文高等计算机与生产技术学校,法国留学院校推荐:安杰文高等计算机与生产技术学校...
  8. tomcat远程调试_docker容器远程debug(Tomcat)
  9. Farey Sequence(欧拉函数板子题)
  10. nodejs gm 中文 linux,nodejs gm drawText使用(中文、字体、大小及颜色)
  11. 黑莓手机刷机经验一点
  12. This tag and its children can be replaced by one TextView/ and a compound drawable
  13. html5+css3.5手机站标准写法,移动端手机网站基本模板
  14. 化云为雨,华为云为什么要深入经济的“毛细血管”?
  15. nyoj-1016-德莱联盟(向量叉乘判断线段相交)
  16. Python 基础 1.0
  17. WPS格式文件转图片格式如何进行操作
  18. 【DeepMind】新算法MuZero在Atari基准上取得了新SOTA效果,成果问鼎Nature
  19. Abaqus idle 3600 seconds 或 Process terminated by external request 问题解决
  20. STM32 DAC 输出正弦波、三角波、方波

热门文章

  1. linux下搭建网桥
  2. requests+正则表达式爬取猫眼电影TOP100
  3. Echarts 折线图完全配置指南 - 手把手教你设置 Echarts 折线图详细教程
  4. 51ND 1432 独木舟
  5. HBase数据压缩方式的介绍与实战
  6. CnOpenData基金公司及从业人员信息数据
  7. 面霸篇:MySQL 35 卷
  8. 数据大爆炸边缘期 让存储告别旧时代
  9. mysql跨版本迁移_不同版本的mysql数据迁移 | 学步园
  10. Microsoft_Toolkit如何使用教程图文(转)