使用go实现简单的文章爬虫功能

需求:

  1. 使用go语言爬取CSDN上某位博主的所有博客

分析:

1、 对于一般的博客文章,主要采集文章标题、发表时间、文章标签、文章链接、文章内容

2、一般都会分页展示博客文章

如果是采用单线程的方式,其步骤是:依次获取每一页的文章列表,将获取的列表存入待爬取的队列里,效率低。

如果是采用并发的方式,其步骤是:并发获取每一页的文章列表,将这些列表存入待爬取的文章通道里,加一个goroutine不断的从通道里取文章基本信息并去爬文章内容。

对于简单的单线程,这里不贴代码了,主要着重并发获取。

第一步:根据每一页获取文章列表信息

  1. package main
  2.  
  3. import (
  4.    “github.com/PuerkitoBio/goquery”
  5.    “fmt”
  6. )
  7.  
  8.  
  9. //定向爬某个博客
  10. type Bloginfo struct {
  11.    HomeUrl string
  12.    BaseUrl string
  13.    PageUrl string
  14.  
  15. }
  16.  
  17. type BlogItem struct {
  18.    Title string
  19.    Link string
  20.    GmtPublish string
  21.    Tag string
  22.    Content string
  23. }
  24.  
  25.  
  26. func main() {
  27.  
  28.    blogInfo := Bloginfo{
  29.       “http://blog.csdn.net/testcs_dn”,
  30.       “http://blog.csdn.net”,
  31.       “http://blog.csdn.net/testcs_dn/article/list/%d”}
  32.  
  33.    d := get_page_list(blogInfo, 1)
  34.    fmt.Println(d)
  35.  
  36. }
  37.  
  38. //获取每一页的文章列表
  39. func get_page_list(info Bloginfo, page int) []BlogItem{
  40.    rst := make([]BlogItem,0);
  41.    doc,:= goquery.NewDocument(fmt.Sprintf(info.PageUrl, page))
  42.    if e!= nil {
  43.       panic(e)
  44.       return rst
  45.    }
  46.    doc.Find(“.article_item”).Each(func(int, e *goquery.Selection){
  47.       item := BlogItem{}
  48.       item.Title = e.Find(“.link_title”).Text()
  49.       item.Link,= e.Find(“.link_title a”).Attr(“href”)
  50.       item.Link = info.BaseUrl + item.Link
  51.       item.GmtPublish = e.Find(“.link_postdate”).Text()
  52.       rst = append(rst, item)
  53.    });
  54.    return rst
  55. }

亲测有效

  1. [{
  2.         [置顶]
  3.         作为一个程序员我为什么要写博客?            
  4.          /testcs_dn/article/details/51231922 20160424 14:08 } {
  5.         [置顶]
  6.         有学历的程序员永远不懂没学历的痛,就像白天不懂夜的黑            
  7.          /testcs_dn/article/details/51170327 20160417 21:06 } {
  8.         [置顶]
  9.         【读评】为什么你有10年经验,但成不了专家?            
  10.          /testcs_dn/article/details/51158562 20160416 14:17 } {
  11.         [置顶]
  12.         After 500:写500篇博客其实和写一篇是一样的            
  13.          /testcs_dn/article/details/50791702 20160304 09:07 } {
  14.         [置顶]
  15.         C#软件开发实例.私人订制自己的屏幕截图工具(一)功能概览            
  16.          /testcs_dn/article/details/23169549 20140408 11:50 } {
  17.         [置顶]
  18.         MySQL学习(二)图形界面管理工具Navicat for MySQL安装和使用            
  19.          /testcs_dn/article/details/21122035 20140312 23:17 } {
  20.         [置顶]
  21.         ThinkPHP学习(一) WindowsNginx+PHP5+ThinkPHP_3.2.1的安装与配置            
  22.          /testcs_dn/article/details/21036345 20140311 21:20 } {
  23.         TFS部署:create_block allocate space error. ret: 1, error: 28, error desc: No space left on device            
  24.          /testcs_dn/article/details/51955536 20160719 15:06 } {
  25.         TFS部署:ERROR create_fs_dir (blockfile_manager.cpp:1191) make extend dir error. ret: 1, error: 17            
  26.          /testcs_dn/article/details/51954873 20160719 13:14 } {
  27.         2016年中总结、半年总结            
  28.          /testcs_dn/article/details/51841165 20160717 22:12 } {
  29.         TFS安装:base_packet.cpp:246: 错误:从类型‘const char*’到类型‘pthread_t’的转换无效            
  30.          /testcs_dn/article/details/51911785 20160714 18:37 } {
  31.         CentOS 6.5 安装 Redis 执行 make #error “Newer version of jemalloc required”            
  32.          /testcs_dn/article/details/51867879 20160709 18:27 } {
  33.         service mysqld start MySQL Daemon failed to start.            
  34.          /testcs_dn/article/details/51811900 20160704 10:26 } {
  35.         我如何添加一个空目录到Git仓库?            
  36.          /testcs_dn/article/details/51811974 20160704 10:24 } {
  37.         IntelliJ IDEA 简单的项目配置            
  38.          /testcs_dn/article/details/51815950 20160703 17:18 } {
  39.         IntelliJ IDEA 的智能编码功能            
  40.          /testcs_dn/article/details/51815933 20160703 17:12 } {
  41.         IntelliJ IDEA 快速入门指南            
  42.          /testcs_dn/article/details/51759461 20160703 17:05 } {
  43.         熟悉 IntelliJ IDEA 的主界面            
  44.          /testcs_dn/article/details/51814897 20160703 13:35 } {
  45.         centos 删除文件夹 permission denied, xxx is not in the sudoers file.            
  46.          /testcs_dn/article/details/51814585 20160703 11:56 } {
  47.         IntelliJ IDEA 运行你的第一个Java应用程序            
  48.          /testcs_dn/article/details/51793511 20160630 22:03 } {
  49.         js跨域交互(jQuery+php)之jsonp使用心得            
  50.          /testcs_dn/article/details/51785002 20160630 13:14 } {
  51.         Linux 下安装IntelliJ IDEA Community Edition            
  52.          /testcs_dn/article/details/51776058 20160628 18:45 } {
  53.         Mac OS 下安装IntelliJ IDEA Community Edition            
  54.          /testcs_dn/article/details/51771422 20160627 21:48 } {
  55.         IntelliJ IDEA的安装和启动            
  56.          /testcs_dn/article/details/51755616 20160624 21:48 } {
  57.         WindowsXP下安装IntelliJ IDEA Ultimate Edition            
  58.          /testcs_dn/article/details/51754979 20160624 18:32 } {
  59.         IntelliJ IDEA的安装环境要求            
  60.          /testcs_dn/article/details/51741298 20160623 15:11 } {
  61.         Windows7下安装IntelliJ IDEA Community Edition 2016.1.3(64)            
  62.          /testcs_dn/article/details/51742613 20160623 14:57 }]

第二步、并发获取

  1. package main
  2.  
  3. import (
  4.    “github.com/PuerkitoBio/goquery”
  5.    “fmt”
  6.    “sync”
  7. )
  8.  
  9.  
  10. //定向爬某个博客
  11. type Bloginfo struct {
  12.    HomeUrl string
  13.    BaseUrl string
  14.    PageUrl string
  15.  
  16. }
  17.  
  18. type BlogItem struct {
  19.    Title string
  20.    Link string
  21.    GmtPublish string
  22.    Tag string
  23.    Content string
  24.  
  25. }
  26.  
  27.  
  28. func main() {
  29.  
  30.    blogInfo := Bloginfo{
  31.       “http://blog.csdn.net/testcs_dn”,
  32.       “http://blog.csdn.net”,
  33.       “http://blog.csdn.net/testcs_dn/article/list/%d”}
  34.  
  35.    pages := 20
  36.  
  37.    result := make([]BlogItem,0)
  38.  
  39.    articleChannel := make(chan []BlogItem,10)
  40.  
  41.  
  42.    wg := sync.WaitGroup{}
  43.    for i:=0;i<pages;i++ {
  44.       wg.Add(1)
  45.       go func(page int){
  46.          fmt.Println(“down list”)
  47.          articleChannel <- get_page_list(blogInfo, page)
  48.          wg.Done()
  49.       }(i)
  50.    }
  51.  
  52.    go func(){
  53.       wg.Wait()
  54.       fmt.Println(“closed”)
  55.       close(articleChannel)
  56.    }()
  57.  
  58.    for articleLists := range articleChannel {
  59.       //爬取内容
  60.       for _,item := range articleLists {
  61.          fmt.Println(“下载…” + item.Link)
  62.          result = append(result, get_article(item))
  63.       }
  64.    }
  65.    fmt.Println(result)
  66. }
  67.  
  68. //获取每一页的文章列表
  69. func get_page_list(info Bloginfo, page int) []BlogItem{
  70.    rst := make([]BlogItem,0);
  71.    doc,:= goquery.NewDocument(fmt.Sprintf(info.PageUrl, page))
  72.    if e!= nil {
  73.       panic(e)
  74.       return rst
  75.    }
  76.    doc.Find(“.article_item”).Each(func(int, e *goquery.Selection){
  77.       item := BlogItem{}
  78.       item.Title = e.Find(“.link_title”).Text()
  79.       item.Link,= e.Find(“.link_title a”).Attr(“href”)
  80.       item.Link = info.BaseUrl + item.Link
  81.       item.GmtPublish = e.Find(“.link_postdate”).Text()
  82.       rst = append(rst, item)
  83.    });
  84.    return rst
  85. }
  86.  
  87. //获取文章内容
  88. func get_article(item BlogItem) BlogItem {
  89.    doc, e := goquery.NewDocument(item.Link)
  90.  
  91.    if e!= nil {
  92.       panic(e)
  93.    }
  94.  
  95.    item.Content,= doc.Find(“#article_details”).Html()
  96.    item.Tag = doc.Find(“.link_categories”).Text()
  97.    return item
  98. }

Golang实现的一个并发爬虫框架

 

利用golan能简单编写并发程序和擅长网络编程的特性实现了一个并发爬虫框架。

一些特点

  • 轻量,易使用
  • 自定义解析库(方法),自定义数据处理方式
  • 组件独立模块化,易扩展,并发实现
  • 多规则,参数支持(爬行深度,http制定等)

基本结构

包含四个组件和一个控制中心,参考了Scarpy的架构,如图

01

Controller控制器

控制器负责调度整个爬虫的运行流程和协调各组件间的工作,包括组件的初始化,数据传递,停止等,使各组件的工作专注于自己的职责

Downloader下载器

下载器是爬虫与互联网交互的部分,把请求通过http与目标交互,获得响应后封装输送至分析器进行解析。这里会提供到对http请求的各种包装(Header, Cookie)接口。

Analyzer分析器

分析器对http响应进行解析,这里会让框架使用者自定义解析方法,提取所需要抓取的数据和下一步请求的链接,把这些数据送至处理器助理,还有对http响应的一些过滤等。

Porcessor处理器

处理器对分析器输送出来的数据和链接进行处理。数据会由使用者提供的方法来进行持久化储存,可选择文件,数据库等多种形式,这里把网络处理与数据储存细节分离开。而链接会进行筛选(去重, host控制),封装为请求以后发送至下载器到下一个流程。处理器还对通道传输有一定的缓存作用。

Monitor监视器

监视器对爬虫整个运行状态进行监控,包括一些实时的抓取数据,组件间数据传输状态,根据情况再优化爬取流程。

四个组件中除了监视器,其他都能由控制器调度拥有多个实例同时工作,得益于golang的goroutine和channel能够简洁的实现。各组件的职责专一降低了框架的耦合性。数据通过channel传输,

部分实现

分析器接口

type Parser func(httpRes *http.Response) ([]string, []basic.Item)

type Analyzer struct {
    linklist []string
    itemlist []basic.Item
}

type GenAnalyzer interface {
    Analyze(httpRes *http.Response, parser Parser) ([]string, []basic.Item)
}

分析器的接口,数据结构,和自定义的解析方法,Parser解析函数由使用者定义并传入分析器,接受一个response, 返回linklist, itemlist,这里使其与框架分离,是使用者按照自己的习惯和爱好选择解析方法(有类似于jquery解析方法的github.com/PuerkitoBio/goquery包,xpath语言的launchpad.net/xmlpath包等等)。对item的储存方法的接口一样的处理

爬虫停止流程

由于涉及多个channel,关闭通道的顺序尤为重要,因为本身也是靠循环通道获取值来流通整个爬虫流程的数据交流。在非意外情况下,当爬行深度大于指定值是发送停止信号,这是在Processor中出现,这时候应该关闭请求通道,按照channel的特点,去除通道的所有值时才会退出循环,退出循环时关闭响应通道。同理在响应通道数据接收完毕后跳出循环关闭link和item通道。

ReqChannel -> ResChannel -> LinkChannel && ItemChannel

工作池

没有去实现类似线程池的goroutine池,一个工作池已经能够完成任务。工作池接收一个work函数,以num个goroutine去运行,在work通过channel来控制阻塞和流通。这里也可以很容易扩展work为接口。

func (self *WorkPool) Pool(num int, work func()) {
    for w := 0; w < num; w++ {
        go work()
    }
}

全局配置

一些可以默认又提供自定义的全局参数配置,以下是一部分:

type config struct {
    Name             string
    StartUrl         string
    RequestMethod    string
    HttpHeader       map[string]string
    DownloaderNumber int
    AnalyzerNumber   int
    ProcessorNumber  int
    ReqChanLength    int
    ResChanLength    int
    LinkChanLength   int
    ItemChanLength   int
}

爬虫启动后会执行初始化,如果没有在main函数里配置相关选项将会使用一个默认的参数。使用者可以根据监视器反映的情况来调整这些参数

快速使用

下面是一个简单的例子

func main() {
    //创建一个控制器,这里有4个必须给予的参数:
    //爬取的初始url,爬取深度,解析函数,储存函数
    controller := controller.NewController("http://www.ccse.uestc.edu.cn/", 1, Parser, Store)
    //启动爬虫
    controller.Go()
}

func Parser(httpRes *http.Response) ([]string, []basic.Item) {
    //两个需要返回的列表
    linklist := make([]string, 0) //下一步需要请求的链接
    itemlist := make([]basic.Item, 0)//保存的数据类型为 map[string]interface{}

    //自定义部分
    //抓取所有链接
    doc, _ := goquery.NewDocumentFromResponse(httpRes)
    doc.Find("a").Each(func(i int, s *goquery.Selection) {
        link, exits := s.Attr("href")
        if exits {
            link = basic.CheckLink(link)
            if link != "" {
                linklist = append(linklist, link)
            }
        }
    })
    //保存每个页面的标题
    title := strings.TrimSpace(doc.Find("head title").Text())
    if title != "" {
        item := make(map[string]interface{})
        item["标题"] = title
        itemlist = append(itemlist, item)
    }

    return linklist, itemlist
}

//储存函数定义
func Store(item basic.Item) {
    //这里只打印抓取的数据
    fmt.Println(item)
}

实现了两个函数用于页面解析和数据处理(函数名任意),然后在main函数里创建一个控制器,再执行Go()函数就能开启爬虫,

还需要做的事

  • 程序不少细节待完善,包括错误处理,日志记录,监控细节,本身的Bug等等
  • 对可自定义的部分进一步完善接口
  • 考虑实现一个更通用,简洁易用的解析方法(按给定的方法去配置,类似Config),真正的解析交给分析器做
  • 在平时的使用中来完善,不断更新迭代

附上源码: Github

~~

  • 越来越喜欢Go的简洁易用,和Python配合基本能满足我的需求了
  • 再次感受到自己更乐意以解决问题的目的编程而不是创造东西的目的,只有面对问题才会激起我的欲望~~
  • Enjoy Coding

Building Grafana from source

This guide will help you create packages from source and get grafana up and running in dev environment. Grafana ships with its own required backend server; also completely open-source. It’s written in Go and has a full HTTP API.

Dependencies

Get Code

Create a directory for the project and set your path accordingly. Then download and install Grafana into your $GOPATH directory

export GOPATH=`pwd`
go get github.com/grafana/grafana

Building the backend

cd $GOPATH/src/github.com/grafana/grafana
go run build.go setup              # (only needed once to install godep)
$GOPATH/bin/godep restore          # (will pull down all golang lib dependencies in your current GOPATH)
go run build.go build              # (or 'go build .')

Building on Windows

The Grafana backend includes Sqlite3 which requires GCC to compile. So in order to compile Grafana on windows you need to install GCC. We recommend TDM-GCC.

Build the Front-end Assets

To build less to css for the frontend you will need a recent version of node (v0.12.0), npm (v2.5.0) and grunt (v0.4.5). Run the following:

npm install
npm install -g grunt-cli
grunt

Recompile backend on source change

To rebuild on source change (requires that you executed godep restore)

go get github.com/Unknwon/bra
bra run

Running Grafana Locally

You can run a local instance of Grafana by running:

./bin/grafana-server

If you built the binary with go run build.go build, run ./bin/grafana-server

If you built it with go build ., run ./grafana

Open grafana in your browser (default http://localhost:3000) and login with admin user (default user/pass = admin/admin).

Developing for Grafana

To add features, customize your config, etc, you’ll need to rebuild on source change (requires that you executed godep restore, as outlined above).

go get github.com/Unknwon/bra
bra run

You’ll also need to run grunt watch to watch for changes to the front-end.

Creating optimized release packages

This step builds linux packages and requires that fpm is installed. Install fpm via gem install fpm.

go run build.go build package

Dev config

Create a custom.ini in the conf directory to override default configuration options. You only need to add the options you want to override. Config files are applied in the order of:

  1. grafana.ini
  2. custom.ini

Learn more about Grafana config options in the Configuration section

Create a pull requests

Please contribute to the Grafana project and submit a pull request! Build new features, write or update documentation, fix bugs and generally make Grafana even more awesome.

Before or after you create a pull request, sign the contributor license agreement. Together we can build amazing software faster.

Grafana 安装

由于 grafana 采用 go 语言编写,且 go 语言本身的跨平台特性,因此 grafana 天生就是一款跨平台的应用。 grafana 官方为 Linux 提供了 deb 和 rpm 两种格式的包,为 Windows 提供了二进制的 zip 包,但却没有为 OS X 提供二进制包。

由于本人是 Mac 用户,本文主要介绍在 Mac 下使用 grafana 的两种常用安装方式。

基于源码安装

注意:通过源码编译需要下载大量第三方依赖,而且有些依赖是没法通过正常网络下载的,因此,如果你想基于源码安装,有一个 VPN 账户是必须的 :)

Go 环境准备

  • 使用 homebrew 安装 Go
  1. brew install go

– 配置 Go 环境变量

  1. export GOROOT=`go env GOROOT`
  2. export GOPATH=~/Documents/GoRepository (该路径根据个人习惯调整)
  3. export GOBIN=$GOPATH/bin
  4. export PATH=$PATH:$GOBIN

– 验证 Go 环境

  1. go version (输出 go version go1.5.3 darwin/amd64 说明 Go 环境准备就绪)

NodeJS 环境准备

  • 使用 homebrew 安装 NodeJS
  1. brew install node
  • 使用 npm 安装 Grunt
  1. npm install -g grunt-cli
  • 验证 NodeJS 环境
  1. node --version (输出 例如:v5.5.0 具体的版本好说明 NodeJS 环境准备就绪 )
  2. grunt --version (输出 例如:grunt-cli v0.1.13 grunt v0.4.5 说明 grunt 工具安装成功)

获取源码并编译

  • 使用 go get 获取源码
  1. go get github.com/grafana/grafana (使用 go get 会将源码下载到 $GOPATH/src 下)
  • 使用 cd 名录进入源码目录
  1. cd $GOPATH/src/github.com/grafana/grafana
  • 编译后端应用
  1. go run build.go setup
  2. $GOPATH/bin/godep restore
  3. go run build.go build
  • 编译前端应用
  1. npm install
  2. grunt

本地运行应用

  • 启动应用
  1. ./bin/grafana-server

2016/03/13 22:06:40 [I] Starting Grafana
2016/03/13 22:06:40 [I] Version: 3.0.0-pre1, Commit: v2.6.0-1366-g6670e6c, Build date: 2016-03-09 23:10:46 +0800 CST
2016/03/13 22:06:40 [I] Configuration Info
Config files:
Paths:
home: /Users/gavin/Documents/GoRepository/src/github.com/grafana/grafana
data: /Users/gavin/Documents/GoRepository/src/github.com/grafana/grafana/data
logs: /Users/gavin/Documents/GoRepository/src/github.com/grafana/grafana/data/log
plugins: data/plugins

2016/03/13 22:06:40 [I] Database: sqlite3
2016/03/13 22:06:40 [I] Migrator: Starting DB migration
2016/03/13 22:06:40 [I] Plugins: Scan starting
2016/03/13 22:06:40 [I] Listen: http://0.0.0.0:3000

  • 访问应用
  1. 打开浏览器输入:localhost:3000 (默认用户名/密码: admin/admin
  • 登录前

before login

  • 登录后

after login

基于 Docker 镜像安装

http://huhz1986.leanote.com/post/Grafana-%E5%AE%89%E8%A3%85

Golang Http Server源码阅读

这篇文章出现的理由是业务上需要创建一个Web Server。创建web是所有语言出现必须实现的功能之一了。在nginx+fastcgi+php广为使用的今天,这里我们不妨使用Go来进行web服务器的搭建。

前言

使用Go搭建Web服务器的包有很多,大致有下面几种方法,直接使用net包,使用net.http包,使用第三方包(比如gorilla)。使用net包就需要从tcp层开始封装,耗费人力物力极大,果断舍弃。直接使用封装好的net.http和第三方包才是上策。这里我们就选择了使用官方提供的net.http包来搭建web服务。另外附带一句,gorilla的第三方包现在使用还是非常广的,文档也是比较全的,有兴趣的同学可以考虑使用一下。

 

建议看这篇文章前先看一下net/http文档 http://golang.org/pkg/net/http/

 

net.http包里面有很多文件,都是和http协议相关的,比如设置cookie,header等。其中最重要的一个文件就是server.go了,这里我们阅读的就是这个文件。

几个重要概念

ResponseWriter: 生成Response的接口

Handler: 处理请求和生成返回的接口

ServeMux: 路由,后面会说到ServeMux也是一种Handler

Conn : 网络连接

 

具体分析

(具体的说明直接以注释形式放在代码中)

几个接口:

Handler

1
2
3
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)  // 具体的逻辑函数
}

实现了handler接口的对象就意味着往server端添加了处理请求的逻辑。

下面是三个接口(ResponseWriter, Flusher, Hijacker):

ResponseWriter, Flusher, Hijacker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ResponseWriter的作用是被Handler调用来组装返回的Response的
type ResponseWriter interface {
    // 这个方法返回Response返回的Header供读写
    Header() Header
    // 这个方法写Response的Body
    Write([]byte) (int, error)
    
    // 这个方法根据HTTP State Code来写Response的Header
    WriteHeader(int)
}
// Flusher的作用是被Handler调用来将写缓存中的数据推给客户端
type Flusher interface {
    // 这个方法将写缓存中数据推送给客户端
    Flush()
}
// Hijacker的作用是被Handler调用来关闭连接的
type Hijacker interface {
    // 这个方法让调用者主动管理连接
    Hijack() (net.Conn, *bufio.ReadWriter, error)
}
1
 

response

实现这三个接口的结构是response(这个结构是http包私有的,在文档中并没有显示,需要去看源码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// response包含了所有server端的http返回信息
type response struct {
    conn          *conn         // 保存此次HTTP连接的信息
    req           *Request // 对应请求信息
    chunking      bool     // 是否使用chunk
    wroteHeader   bool     // header是否已经执行过写操作
    wroteContinue bool     // 100 Continue response was written
    header        Header   // 返回的http的Header
    written       int64    // Body的字节数
    contentLength int64    // Content长度
    status        int      // HTTP状态
    needSniff     bool     // 是否需要使用sniff。(当没有设置Content-Type的时候,开启sniff能根据HTTP body来确定Content-Type)
    
    closeAfterReply bool     //是否保持长链接。如果客户端发送的请求中connection有keep-alive,这个字段就设置为false。
    requestBodyLimitHit bool //是否requestBody太大了(当requestBody太大的时候,response是会返回411状态的,并把连接关闭)
}

 

在response中是可以看到

1
2
3
4
5
func (w *response) Header() Header
func (w *response) WriteHeader(code int)
func (w *response) Write(data []byte) (n int, err error)
func (w *response) Flush()
func (w *response) Hijack() (rwc net.Conn, buf *bufio.ReadWriter, err error)

这么几个方法。所以说response实现了ResponseWriter,Flusher,Hijacker这三个接口

 

HandlerFunc

handlerFunc是经常使用到的一个type

1
2
3
4
5
6
7
// 这里将HandlerFunc定义为一个函数类型,因此以后当调用a = HandlerFunc(f)之后, 调用a的ServeHttp实际上就是调用f的对应方法
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

 

这里需要多回味一下了,这个HandlerFunc定义和ServeHTTP合起来是说明了什么?说明HandlerFunc的所有实例是实现了ServeHttp方法的。另,实现了ServeHttp方法就是什么?实现了接口Handler!

 

所以你以后会看到很多这样的句子:

1
2
3
4
5
func AdminHandler(w ResponseWriter, r *Request) {
    ...
}
handler := HandlerFunc(AdminHandler)
handler.ServeHttp(w,r)

 

请不要讶异,你明明没有写ServeHttp,怎么能调用呢? 实际上调用ServeHttp就是调用AdminHandler。

好吧,理解这个也花了我较长时间,附带上一个play.google写的一个小例子

http://play.golang.org/p/nSt_wcjc2u

有兴趣继续研究的同学可以继续试验下去

 

如果你理解了HandlerFunc,你对下面两个句子一定不会讶异了

1
2
3
func NotFound(w ResponseWriter, r *Request) { Error(w, "404 page not found", StatusNotFound) }
func NotFoundHandler() Handler { return HandlerFunc(NotFound) }

 

下面接着看Server.go

ServerMux结构

它就是http包中的路由规则器。你可以在ServerMux中注册你的路由规则,当有请求到来的时候,根据这些路由规则来判断将请求分发到哪个处理器(Handler)。

它的结构如下:

1
2
3
4
type ServeMux struct {
    mu sync.RWMutex   //锁,由于请求设计到并发处理,因此这里需要一个锁机制
    m  map[string]muxEntry  // 路由规则,一个string对应一个mux实体,这里的string就是我注册的路由表达式
}
1
 

下面看一下muxEntry

1
2
3
4
type muxEntry struct {
    explicit bool   // 是否精确匹配
    h        Handler // 这个路由表达式对应哪个handler
}
1
 

看到这两个结构就应该对请求是如何路由的有思路了:

当一个请求request进来的时候,server会依次根据ServeMux.m中的string(路由表达式)来一个一个匹配,如果找到了可以匹配的muxEntry,就取出muxEntry.h,这是个handler,调用handler中的ServeHTTP(ResponseWriter, *Request)来组装Response,并返回。

 

ServeMux定义的方法有:

1
2
3
4
5
6
func (mux *ServeMux) match(path string) Handler   //根据path获取Handler
func (mux *ServeMux) handler(r *Request) Handler  //根据Request获取Handler,内部实现调用match
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) //!!这个说明,ServeHttp也实现了Handler接口,它实际上也是一个Handler!内部实现调用handler
func (mux *ServeMux) Handle(pattern string, handler Handler) //注册handler方法
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request))  //注册handler方法(直接使用func注册)

 

在godoc文档中经常见到的DefaultServeMux是http默认使用的ServeMux

var DefaultServeMux = NewServeMux()

如果我们没有自定义ServeMux,系统默认使用这个ServeMux。

 

换句话说,http包外层(非ServeMux)中提供的几个方法:

1
2
3
4
func Handle(pattern string, handler Handler) { DefaultServeMux.Handle(pattern, handler) }
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    DefaultServeMux.HandleFunc(pattern, handler)
}

实际上就是调用ServeMux结构内部对应的方法。

 

Server

下面还剩下一个Server结构

1
2
3
4
5
6
7
8
type Server struct {
    Addr           string        // 监听的地址和端口
    Handler        Handler       // 所有请求需要调用的Handler(实际上这里说是ServeMux更确切)如果为空则设置为DefaultServeMux
    ReadTimeout    time.Duration // 读的最大Timeout时间
    WriteTimeout   time.Duration // 写的最大Timeout时间
    MaxHeaderBytes int           // 请求头的最大长度
    TLSConfig      *tls.Config   // 配置TLS
}

Server提供的方法有:

1
2
3
func (srv *Server) Serve(l net.Listener) error   //对某个端口进行监听,里面就是调用for进行accept的处理了
func (srv *Server) ListenAndServe() error  //开启http server服务,内部调用Serve
func (srv *Server) ListenAndServeTLS(certFile, keyFile string) error //开启https server服务,内部调用Serve

 

当然Http包也直接提供了方法供外部使用,实际上内部就是实例化一个Server,然后调用ListenAndServe方法

1
2
func ListenAndServe(addr string, handler Handler) error   //开启Http服务
func ListenAndServeTLS(addr string, certFile string, keyFile string, handler Handler) error //开启HTTPs服务

 

具体例子分析

下面根据上面的分析,我们对一个例子我们进行阅读。这个例子搭建了一个最简易的Server服务。当调用http://XXXX:12345/hello的时候页面会返回“hello world”

1
2
3
4
5
6
7
8
9
10
11
12
func HelloServer(w http.ResponseWriter, req *http.Request) {
    io.WriteString(w, "hello, world!\n")
}
func main() {
    http.HandleFunc("/hello", HelloServer)
    err := http.ListenAndServe(":12345", nil)
    if err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}

 

首先调用Http.HandleFunc

按顺序做了几件事:

1 调用了DefaultServerMux的HandleFunc

2 调用了DefaultServerMux的Handle

3 往DefaultServeMux的map[string]muxEntry中增加对应的handler和路由规则

 

其次调用http.ListenAndServe(“:12345”, nil)

按顺序做了几件事情:

1 实例化Server

2 调用Server的ListenAndServe()

3 调用net.Listen(“tcp”, addr)监听端口

4 启动一个for循环,在循环体中Accept请求

5 对每个请求实例化一个Conn,并且开启一个goroutine为这个请求进行服务go c.serve()

6 读取每个请求的内容w, err := c.readRequest()

7 判断header是否为空,如果没有设置handler(这个例子就没有设置handler),handler就设置为DefaultServeMux

8 调用handler的ServeHttp

9 在这个例子中,下面就进入到DefaultServerMux.ServeHttp

10 根据request选择handler,并且进入到这个handler的ServeHTTP

mux.handler(r).ServeHTTP(w, r)

11 选择handler:

A 判断是否有路由能满足这个request(循环遍历ServerMux的muxEntry)

B 如果有路由满足,调用这个路由handler的ServeHttp

C 如果没有路由满足,调用NotFoundHandler的ServeHttp

后记

对于net.http包中server的理解是非常重要的。理清serverMux, responseWriter, Handler, HandlerFunc等常用结构和函数是使用go web的重要一步。个人感觉由于go中文档较少,像这样有点复杂的包,看godoc的效果就远不如直接看代码来的快和清晰了。实际上在理解了http包后,才会对godoc中出现的句子有所理解。后续还会写一些文章关于使用net.http构建web server的。请期待之。

golang中并发sync和channel

golang中实现并发非常简单,只需在需要并发的函数前面添加关键字"go”,但是如何处理go并发机制中不同goroutine之间的同步与通信,golang 中提供了sync包和channel机制来解决这一问题.

sync 包提供了互斥锁这类的基本的同步原语.除 Once 和 WaitGroup 之外的类型大多用于底层库的例程。更高级的同步操作通过信道与通信进行。

type Cond
    func NewCond(l Locker) *Cond
    func (c *Cond) Broadcast()
    func (c *Cond) Signal()
    func (c *Cond) Wait()
type Locker
type Mutex
    func (m *Mutex) Lock()
    func (m *Mutex) Unlock()
type Once
    func (o *Once) Do(f func())
type Pool
    func (p *Pool) Get() interface{}
    func (p *Pool) Put(x interface{})
type RWMutex
    func (rw *RWMutex) Lock()
    func (rw *RWMutex) RLock()
    func (rw *RWMutex) RLocker() Locker
    func (rw *RWMutex) RUnlock()
    func (rw *RWMutex) Unlock()
type WaitGroup
    func (wg *WaitGroup) Add(delta int)
    func (wg *WaitGroup) Done()
    func (wg *WaitGroup) Wait()

而golang中的同步是通过sync.WaitGroup来实现的.WaitGroup的功能:它实现了一个类似队列的结构,可以一直向队列中添加任务,当任务完成后便从队列中删除,如果队列中的任务没有完全完成,可以通过Wait()函数来出发阻塞,防止程序继续进行,直到所有的队列任务都完成为止.

WaitGroup总共有三个方法:Add(delta int), Done(), Wait()。

Add:添加或者减少等待goroutine的数量

Done:相当于Add(-1)

Wait:执行阻塞,直到所有的WaitGroup数量变成0

具体例子如下:

package main

import (
	"fmt"
	"sync"
)

var waitgroup sync.WaitGroup

func Afunction(shownum int) {
	fmt.Println(shownum)
	waitgroup.Done() //任务完成,将任务队列中的任务数量-1,其实.Done就是.Add(-1)
}

func main() {
	for i := 0; i < 10; i++ {
		waitgroup.Add(1) //每创建一个goroutine,就把任务队列中任务的数量+1
		go Afunction(i)
	}
	waitgroup.Wait() //.Wait()这里会发生阻塞,直到队列中所有的任务结束就会解除阻塞
}

使用场景:  程序中需要并发,需要创建多个goroutine,并且一定要等这些并发全部完成后才继续接下来的程序执行.WaitGroup的特点是Wait()可以用来阻塞直到队列中的所有任务都完成时才解除阻塞,而不需要sleep一个固定的时间来等待.但是其缺点是无法指定固定的goroutine数目.

Channel机制:

相对sync.WaitGroup而言,golang中利用channel实习同步则简单的多.channel自身可以实现阻塞,其通过<-进行数据传递,channel是golang中一种内置基本类型,对于channel操作只有4种方式:

创建channel(通过make()函数实现,包括无缓存channel和有缓存channel);

向channel中添加数据(channel<-data);

从channel中读取数据(data<-channel);

关闭channel(通过close()函数实现,关闭之后无法再向channel中存数据,但是可以继续从channel中读取数据)

channel分为有缓冲channel和无缓冲channel,两种channel的创建方法如下:

var ch = make(chan int) //无缓冲channel,等同于make(chan int ,0)

var ch = make(chan int,10) //有缓冲channel,缓冲大小是5

其中无缓冲channel在读和写是都会阻塞,而有缓冲channel在向channel中存入数据没有达到channel缓存总数时,可以一直向里面存,直到缓存已满才阻塞.由于阻塞的存在,所以使用channel时特别注意使用方法,防止死锁的产生.例子如下:

无缓存channel:

package main

import "fmt"

func Afuntion(ch chan int) {
	fmt.Println("finish")
	<-ch
}

func main() {
	ch := make(chan int) //无缓冲的channel
	go Afuntion(ch)
	ch <- 1
	
	// 输出结果:
	// finish
}

代码分析:首先创建一个无缓冲channel ch, 然后执行 go Afuntion(ch),此时执行<-ch,则Afuntion这个函数便会阻塞,不再继续往下执行,直到主进程中ch<-1向channel ch 中注入数据才解除Afuntion该协程的阻塞.

package main

import "fmt"

func Afuntion(ch chan int) {
	fmt.Println("finish")
	<-ch
}

func main() {
	ch := make(chan int) //无缓冲的channel
	//只是把这两行的代码顺序对调一下
	ch <- 1
	go Afuntion(ch)

	// 输出结果:
	// 死锁,无结果
}

代码分析:首先创建一个无缓冲的channel, 然后在主协程里面向channel ch 中通过ch<-1命令写入数据,则此时主协程阻塞,就无法执行下面的go Afuntions(ch),自然也就无法解除主协程的阻塞状态,则系统死锁

总结:
对于无缓存的channel,放入channel和从channel中向外面取数据这两个操作不能放在同一个协程中,防止死锁的发生;同时应该先利用go 开一个协程对channel进行操作,此时阻塞该go 协程,然后再在主协程中进行channel的相反操作(与go 协程对channel进行相反的操作),实现go 协程解锁.即必须go协程在前,解锁协程在后.

带缓存channel:
对于带缓存channel,只要channel中缓存不满,则可以一直向 channel中存入数据,直到缓存已满;同理只要channel中缓存不为0,便可以一直从channel中向外取数据,直到channel缓存变为0才会阻塞.

由此可见,相对于不带缓存channel,带缓存channel不易造成死锁,可以同时在一个goroutine中放心使用,

close():
close主要用来关闭channel通道其用法为close(channel),并且实在生产者的地方关闭channel,而不是在消费者的地方关闭.并且关闭channel后,便不可再想channel中继续存入数据,但是可以继续从channel中读取数据.例子如下:

package main

import "fmt"

func main() {
    var ch = make(chan int, 20)
    for i := 0; i < 10; i++ {
        ch <- i
    }
    close(ch)
    //ch <- 11 //panic: runtime error: send on closed channel
    for i := range ch {
        fmt.Println(i) //输出0 1 2 3 4 5 6 7 8 9
    }
}

channel阻塞超时处理:
goroutine有时候会进入阻塞情况,那么如何避免由于channel阻塞导致整个程序阻塞的发生那?解决方案:通过select设置超时处理,具体程序如下:

package main

 import (
    "fmt"
    "time"
)

func main() {
    c := make(chan int)
    o := make(chan bool)
    go func() {
        for {
            select {
            case i := <-c:
                fmt.Println(i)
            case <-time.After(time.Duration(3) * time.Second):    //设置超时时间为3s,如果channel 3s钟没有响应,一直阻塞,则报告超时,进行超时处理.
                fmt.Println("timeout")
                o <- true
                break
            }
        }
    }()
    <-o
}


golang 并发总结:
并发两种方式:sync.WaitGroup,该方法最大优点是Wait()可以阻塞到队列中的所有任务都执行完才解除阻塞,但是它的缺点是不能够指定并发协程数量.
channel优点:能够利用带缓存的channel指定并发协程goroutine,比较灵活.但是它的缺点是如果使用不当容易造成死锁;并且他还需要自己判定并发goroutine是否执行完.

但是相对而言,channel更加灵活,使用更加方便,同时通过超时处理机制可以很好的避免channel造成的程序死锁,因此利用channel实现程序并发,更加方便,更加易用.

参考文献:

http://studygolang.com/articles/319

http://studygolang.com/articles/267