Go资深工程师讲解(慕课) 005

005 标准库

http

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

import (
    "fmt"
    "net/http"
    "net/http/httputil"
)

func main() {
    resp, err := http.Get("https://www.baidu.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)
}

设置手机浏览 set user-agent

package main

import (
    "fmt"
    "net/http"
    "net/http/httputil"
)

func main() {
    request, err := http.NewRequest(http.MethodGet, "https://www.baidu.com", nil)
    request.Header.Add("User-Agent", "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Mobile Safari/537.36")

    resp, err := http.DefaultClient.Do(request) //http.Get("https://www.baidu.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)
}

go tool pprof http://localhost:8888/debug/pprof/profile
或是引一个包_ "net/http/pprof"启动 web 服务 在地址栏里输入http://localhost:8888/debug/pprof

其它标准库

  • bufio
  • log
  • encoding/json
  • regexp
  • time (channel 用法)
  • strings/math/rand

看标准库的文档
godoc -http :8888 本地启一个服务
https://studygolang.com/pkgdoc

迷宫算法

广度优先算法(应用广泛,综合性强)
已经发现还没探索,放在队列中,探索至终,然后倒着走就最短路径

guangdu

package main

import (
      "fmt"
      "os"
)

//返回二维数组
func readMaze(filename string) [][]int {
      file, err := os.Open(filename)
      if err != nil {
            panic(err)
      }
      defer file.Close()
      var row, col int
      fmt.Fscanf(file, "%d %d", &row, &col)
      maze := make([][]int, row) // row行
      for i := range maze {
            maze[i] = make([]int, col)
            for j := range maze[i] {
                  fmt.Fscanf(file, "%d", &maze[i][j])
            }
      }
      return maze
}

type point struct {
      i, j int
}

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) { //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
}

//四个方向 上左下右
var dirs = [4]point{
      {-1, 0},
      {0, -1},
      {1, 0},
      {0, 1},
}

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
                  }
                  //      steps 填进去
                  curSteps, _ := cur.at(steps)
                  steps[next.i][next.j] = curSteps + 1
                  Q = append(Q, next)
            }

      }
      return steps

}

func main() {
      maze := readMaze("maze/maze.in")
      for _, row := range maze {
            for _, val := range row {
                  fmt.Printf("%3d ", val)
            }
            fmt.Println()
      }
      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()
      }
}
// 途经的点可以倒序遍历一下

简单爬虫

  • 通用爬虫 如 baidu,google
  • 聚焦爬虫 从互联网获取结构化数据
  • go 语言的爬虫库/框架
  • henrylee2cn/pholcus
  • gocrawl
  • colly
  • hu17889/go_spider

技术选型,爬虫的主题(如新闻,博客,社区,我们爬人,QQ 人人,微博,facebook,相亲网站,求职网站)

  • ElasticSearch 作为数据存储
  • Go 语言标准模板库实现 http 数据展示部分

单任务版爬虫

获取并打印所有城市的第一页用户的详细信息
转码 go get -g -v golang.org/x/text
自动识别网页的编码 go get -g -v golang.org/x/net/html

package main

import (
    "bufio"
    "fmt"
    "io"
    "io/ioutil"
    "net/http"

    "golang.org/x/net/html/charset"
    "golang.org/x/text/encoding"
    "golang.org/x/text/transform"
)

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: ", resp.StatusCode)
        //panic("Status Code:")
        return
    }
    //自动转码
    e := determineEncoding(resp.Body)
    utf8Reader := transform.NewReader(resp.Body, e.NewDecoder())
    all, err := ioutil.ReadAll(utf8Reader)
    if err != nil {
        panic(err)
    }
    fmt.Printf("%s\n", all)

}

func determineEncoding(r io.Reader) encoding.Encoding {
    bytes, err := bufio.NewReader(r).Peek(1024)
    if err != nil {
        panic(err)
    }
    e, _, _ := charset.DetermineEncoding(bytes, "")
    return e
}

正则表达式

package main

import (
    "fmt"
    "regexp"
)

const text = "My email is ccmouse@gmail.com"

func main() {
    //re := regexp.MustCompile("ccmouse@gmail.com")
    //用反引号,正则的特殊符号不用再转意了
    re := regexp.MustCompile(`[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+`)
    match := re.FindString(text)
    fmt.Println(match)
}

读取 json 文件并 parse 成对应的对象

package main

import (
    "encoding/json"
    "fmt"
    "os"

)

type CityObj struct {
    LinkContent string `json:"linkContent"`
    LinkURL string
}

type CityGroup struct {
    CityList []CityObj
    Order string
}

type RtnData struct {
    CityData []CityGroup
}

func main() {
    path,_:=os.Getwd()
    fmt.Println(path)
    //var jObj interface{}
    var jObj RtnData
    file, err := os.Open("./cities.json")
    if err != nil {
        panic(err)
    }
    //file stat
    fi, _ := file.Stat()
    buffer := make([]byte, fi.Size())
    _, err = file.Read(buffer)
    if err != nil {
        panic(err)
    }
    err = json.Unmarshal(buffer, &jObj)
    fmt.Println(jObj.CityData[0].CityList[0])
}

通过正则来爬取数据

package main

import (
    "bufio"
    "fmt"
    "io"
    "io/ioutil"
    "net/http"
    "regexp"

    "golang.org/x/net/html/charset"
    "golang.org/x/text/encoding"
    "golang.org/x/text/encoding/unicode"
    "golang.org/x/text/transform"
)

func main() {
    //所有城市第一页用户
    url := "http://www.zhenai.com/zhenghun"
    resp, err := http.Get(url)
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        fmt.Println("Error: ", resp.StatusCode)
        panic("Status Code:")

    }
    //自动转码
    e := determineEncoding(resp.Body)
    utf8Reader := transform.NewReader(resp.Body, e.NewDecoder())
    all, err := ioutil.ReadAll(utf8Reader)
    if err != nil {
        //panic(err)
        fmt.Printf("%v\n", err)
        return
    }

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

}

func determineEncoding(r io.Reader) encoding.Encoding {
    bytes, err := bufio.NewReader(r).Peek(1024)
    if err != nil {
        //panic(err)
        return unicode.UTF8
    }
    e, _, _ := charset.DetermineEncoding(bytes, "")
    return e
}

func printCityList(contents []byte) {
    //re := regexp.MustCompile(`<a target="_blank" href="http://www.zhenai.com/zhenghun/[0-9a-z]+"[^>]*>[^<]+</a>`)
    re := regexp.MustCompile(`<a href="http://www.zhenai.com/zhenghun/[0-9a-z]+"[^>]*>[^<]+</a>`) // 470个城市
    matches := re.FindAll(contents, -1)
    for _,m:=range matches{
        fmt.Printf("%s\n",m)
    }
    fmt.Printf("Matches found: %d\n",len(matches))
}

single_task

爬取数据时需要注意,请求头的设置

  1. 声明 http.Clien{}
  2. 声明 request,err:=http.NewRequest("GET",url,nil)
  3. request.Header.Add
  4. resp, err := client.Do(request)请求替换http.Get(url)
  5. defer resp.Body.Close()

到此单任务版的 查看engin fetcher model zhenai 处个目录的内容

并发版

Fetcher 的输出就是 Parser 的输入,可以抽成一个模块

bingfa_worker.jpg

Scheduler 实现 1:所有 Worker 公用一个输入

见分支simplechen

实现 2 有两个队列 request 队列 和 worker 队列

bingfa_worker_v2

把 request 放到 request 的队列中,为每一个 request 创建一个 go routine,然后让所有的 workder 抢一个 chanel。

其它页面

重复率很高,抓取一个城市,比如上海

e.Run(engine.Request{
    Url:"http://www.zhenai.com/zhenghun/shanghai",
    ParserFunc: parser.ParseCity,
})

url 去重

  • 哈希表(真接存储,占空间--本课使用)
  • 计算 md5 等哈希,再存哈希表
  • 使用 bloom filter 多重哈希结构
  • 使用 redis 等 key-value 存储系统实现分布式去重

profile 的保存

抽象出 Task 的概念 FetchTask,PersistTask 共用一个 Engine,Scheduler,需要创建 FetchWorker,PersistWorker;本项目中显得过重。为每个 Item 创建 goroutine,提交给 ItemSaver;ItemSaver 的速度比 Fetcher 快,类似 SimpleScheduler 的方法即可,我们将采用这种方法。

saver

elastic 与 docker

elastic 全文搜索引擎
本教程使用 docker 集成 elastic install doc

docker pull docker.elastic.co/elasticsearch/elasticsearch:6.7.2

 docker run -e ES_JAVA_OPTS="-Xms256m -Xmx256m" -d -p 9200:9200 -p 9300:9300 -v /home/soft/ES/config/es1.yml:/usr/share/elasticsearch/config/elasticsearch.yml -v /home/soft/ES/data1:/usr/share/elasticsearch/data --name ES01 elasticsearch:5.6.8

 docker run -e ES_JAVA_OPTS="-Xms256m -Xmx256m" -d -p 9201:9201 -p 9301:9301 -v /home/soft/ES/config/es2.yml:/usr/share/elasticsearch/config/elasticsearch.yml -v /home/soft/ES/data2:/usr/share/elasticsearch/data --name ES02 elasticsearch:5.6.8

 docker run -e ES_JAVA_OPTS="-Xms256m -Xmx256m" -d -p 9202:9202 -p 9302:9302 -v /home/soft/ES/config/es3.yml:/usr/share/elasticsearch/config/elasticsearch.yml -v /home/soft/ES/data3:/usr/share/elasticsearch/data --name ES03 elasticsearch:5.6.8

#   本例运行如下
docker run -e ES_JAVA_OPTS="-Xms256m -Xmx256m" -d -p 9200:9200 -p 9300:9300 -v /Users/willzhao/Documents/Develop/docker/elasticsearch/6.8/data:/usr/share/elasticsearch/data --name ES_6.8 elasticsearch:6.8.0
# http://localhost:9200/ 可以访问

比如我们创建 course 库
添加记录我们可以用 put 或 post course/1 course/2 添加 json 数据,可以用 get 获取全部数据,或用 get 加参数course/_search?,
不加 id 要用 post

  • <server>:9200/index/type/id
  • index 相当于 database
  • type 相当于 table
  • 我们不需要预先创建 index 和 type (IK 插件 有时间了解一下)
  • 使用 REST 接口
  • PUT/POST 创建/修改数据,使用 post 可以省略 id
  • GET 获取数据
  • GET <index>/<type>/_search?q='参数'

本课中的 save()使用elasticsearch client
go 社区版
https://gopkg.in/olivere/elastic.v6

elastic_client.jpg

# 先安装v6(我认为是基础
go get -v -u github.com/olivere/elastic
# 引这个的包 本例中用的是v6 在查询全
# 这个是v6的client https://olivere.github.io/elastic/ 请仔细阅读这个文档
go get gopkg.in/olivere/elastic.v6

html/template

分布式系统

  • 多个结点
  • 容错
  • 可扩展性(性能)
  • 固有分布性
  • 消息传递
  • 节点具有私有存储
  • 易于开发
  • 可扩展性(功能)
  • 对比:并行计算
  • 消息传递的方法
    • RESt
    • RPC
    • 中间件(messageQ)
  • 使用场景
    • 对外:Rest
    • 模块内部:RPC
    • 模块之间:中间件,REST
  • 完成特定需求

分布式架构 vs 微服务架构

分布式:指导节点之间如何通信
微服务:鼓励按业务划分模块

多层架构 vs 微服务架构

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

分部式爬虫

  • 限流问题
  • 单节点能够承受的流量有限
  • 将 worker 放到不同的节点
  • 去重问题
  • 分布式去重将去重放到 worker 上
  • 数据存储问题

jsonRPC

package rpcdemo

import "errors"

// Service.Method

type DemoService struct {}

type Args struct {
    A,B int
}

func (DemoService) Dive(args Args,result *float64) error  {
    if args.B==0 {
        return errors.New("division by zero.")
    }
     *result = float64(args.A)/float64(args.B)
     return nil
}
// server
package main

import (
    rpcdemo "gobasic/rpc"
    "log"
    "net"
    "net/rpc"
    "net/rpc/jsonrpc"
)

func main() {
    rpc.Register(rpcdemo.DemoService{})

    listener, err := net.Listen("tcp", ":1234")
    if err != nil {
        panic(err)
    }
    for {
        conn, err := listener.Accept()
        if err != nil {
            log.Printf("accept error: %v", err)
            continue
        }

        go jsonrpc.ServeConn(conn)
    }
}

可以通过 telnet localhost 1234 来测试一下,发 json 数据如下

{"method":"DemoService.Dive","params":[{"A":3,"B":4}],"id":1}
// 返回结果
{"id":1,"result":0.75,"error":null}

创建一个 client 来调用

// client/main.go
package main

import (
    "fmt"
    rpcdemo "gobasic/rpc"
    "net"
    "net/rpc/jsonrpc"
)

func main() {
    conn, err := net.Dial("tcp", ":1234")

    if err != nil {
        panic(err)
    }
    client := jsonrpc.NewClient(conn)
    var result float64
    err = client.Call("DemoService.Dive", rpcdemo.Args{10, 3}, &result)
    fmt.Println(result, err)

    err = client.Call("DemoService.Dive", rpcdemo.Args{10, 0}, &result)
    fmt.Println(result, err)
}

主题测试文章,只做测试使用。发布者:Walker,转转请注明出处:https://www.walker-learn.xyz/archives/6742

(0)
Walker的头像Walker
上一篇 2026年3月6日 15:00
下一篇 2026年3月6日 14:00

相关推荐

  • Go工程师体系课 003

    grpc grpc grpc-go grpc 无缝集成了 protobuf protobuf 习惯用 Json、XML 数据存储格式的你们,相信大多都没听过 Protocol Buffer。 Protocol Buffer 其实是 Google 出品的一种轻量 & 高效的结构化数据存储格式,性能比 Json、XML 真的强!太!多! protobuf…

    后端开发 2026年3月7日
    7900
  • Go工程师体系课 018

    API 网关与持续部署入门(Kong & Jenkins) 对应资料目录《第 2 章 Jenkins 入门》《第 3 章 通过 Jenkins 部署服务》,整理 Kong 与 Jenkins 在企业级持续交付中的实战路径。即便零基础,也能顺着步骤搭建出自己的网关 + 持续部署流水线。 课前导览:什么是 API 网关 API 网关位于客户端与后端微服务…

    后端开发 2026年3月6日
    10200
  • Go工程师体系课 011

    查询的倒排索引 1. 什么是倒排索引? 倒排索引(Inverted Index)是一种数据结构,用于快速查找包含特定词汇的文档。它是搜索引擎的核心技术之一。 1.1 基本概念 正排索引:文档 ID → 文档内容(词列表) 倒排索引:词 → 包含该词的文档 ID 列表 1.2 为什么叫"倒排"? 倒排索引将传统的"文档包含哪些词"的关系倒转为"词出现在哪些文档…

    后端开发 2026年3月7日
    7000
  • Go工程师体系课 008

    订单及购物车 先从库存服务中将 srv 的服务代码框架复制过来,查找替换对应的名称(order_srv) 加密技术基础 对称加密(Symmetric Encryption) 原理: 使用同一个密钥进行加密和解密 就像一把钥匙,既能锁门也能开门 加密速度快,适合大量数据传输 使用场景: 本地文件加密 数据库内容加密 大量数据传输时的内容加密 内部系统间的快速通…

    后端开发 2026年3月6日
    6600
  • Go工程师体系课 014

    rocketmq 快速入门 去我们的各种配置(podman)看是怎么安装的 概念介绍 RocketMQ 是阿里开源、Apache 顶级项目的分布式消息中间件,核心组件: NameServer:服务发现与路由 Broker:消息存储、投递、拉取 Producer:消息生产者(发送消息) Consumer:消息消费者(订阅并消费消息) Topic/Tag:主题/…

    后端开发 2026年3月6日
    7800
简体中文 繁体中文 English