本文适合有Java开发经验且想学习Go的同学,是一篇非常适合入门Go语言的教程,本文只会涉及部分Go的核心内容,不适合想系统学习Go的同学,但也提供了系统学习的方式。
下面是对Go的简单介绍,可以先了解一下,如果有哪些暂时无法理解的可以先跳过,学完【极速入门】之后回头来看就会恍然大悟。
Go是一门语法非常简单的开源编程语言,目的是让程序开发变得更有生产效率。
Go是一门静态类型的编译型语言,通过在不同平台上的相应编译器来实现跨平台特性。
Go是一门广纳百家之长的语言,看起来像既像C++又像Delphi、Python,实则是吸收其他语言的优势,又针对其他语言的痛点进行设计。
它的并发机制使编写能够充分利用多线程的程序变得容易,其新颖的类型系统可实现灵活的模块化程序构建,是一门为大数据、微服务、高并发、云计算而生的通用语言。
https://golang.org/dl/ 选择1.10.3版本下载
IntelliJ IDEA中下载Go插件或者直接下载 GoLand
如果你不习惯Windows的快捷键设置, IDEA-Mac-Keymap-On-Windows 将Mac上的Command键改成左Alt键或许可以帮到你。
让我们正式来体验Go语言,以下所有代码你应该先想一下大致应该如何写,然后看应该如何写,最后自己再写一遍直至跑通,有任何问题及时去搜索理解。
首先还是得从hello world开始。现在IDE中创建一个项目,在项目根目录下创建一个名为 helloworld.go
的文件,输入以下代码:
package main //必须是main包才能定义出一个可执行的入口,main命名与文件名无关 import "fmt" //一个格式化输出的核心库 func main(){ //函数命名格式:func 函数名(参数) 返回值 fmt.Println("hello world!") }
通过IDE的运行按钮或者在命令行中输入 go run helloworld.go
运行它
接下来介绍一个简单的爬虫来体验一部分Go语言的特性。不用马上动手实现它,而是从它开始简单看看Go的语法,Go的并发编程模型,等系统学习之后再来试试能不能实现。
这个爬虫的工作原理如下:首先准备一些想要抓取的网页Url作为种子传给Engine,由Engine负责流程控制,Engine会把Url传给Worker中的Fetcher来下载具体的HTML文件,然后发送给Url对应的Parser解析函数得到想要抓取的信息以及当前页面上可以继续当做种子再次抓取的Url,最后将Url与对应的Parser传给Engine继续抓取。
其中支持并发的Worker与单个Engine传递与接收数据使用的是go runtime与channel实现,可以来感受一下前所未有的并发编程体验。Worker数量越多,爬虫效率越高。
先在根目录下将不同模块的文件夹创建出来,有engine, fetcher, parser三个,然后在根目录把入口程序spider.go写好:
func main() { engine.SimpleEngine{ // engine是我们需要实现的一个程序包,创建engine包中的SimpleEngine结构 WorkerCount: 100, // worker开启数量 }.Run(engine.Request{ // 被调用的Run方法需要被定义在该结构体的内部 Url: "http://www.zhenai.com/zhenghun", //第一个参数是用于被抓取的网页的URL ParserFunc: parser.ParseCityList, //第二个参数是对于不同Url编写的不同的解析器 }) }
在engine文件夹中新建types.go将Request结构体定义:
type Request struct { Url string ParserFunc func([]byte) ParseResult // 解析函数,不同的Url可能对应不同的解析函数,传入HTML的字节数组 // 传出HTML中解析出来的可以继续当成种子的Request和HTML中的有效信息 } type ParseResult struct { Requests []Request //从HTML中解析出来的种子 Items []interface{} //任意类型的数组结构 }
接下来了解一下goruntime与channel。goroutine是Go里的一种轻量级线程——协程。相对线程,协程的优势就在于它非常轻量级,进行上下文切换的代价非常的小,通过go关键字传入一个方法就能开启一个协程。
channel是一种带有类型的变量类型,你可以通过它用channel操作符 <-
在不同的协程之间来发送或者接收值。默认情况下,发送和接收操作在另一端准备好之前都会阻塞。这使得 Go 程可以在没有显式的锁或竞态变量的情况下进行同步,从而大大降低了并发编程的难度,提升了开发效率。
接下来看engine中的核心代码:
type SimpleEngine struct { // 创建engine结构体用以在创建时接受参数,但这并不是构造函数,Go中没有构造函数与析构函数 WorkerCount int } // 为SimpleEngine结构体定义Run方法。SimpleEngine为该方法的值接受者。 // 由于方法经常需要修改它的接收者,指针接收者比值接收者更常用;参数是可变长的类型,用来传入多个种子 func (e SimpleEngine) Run(seeds ...Request) { in := make(chan Request) // 创建种子输入channel,其中可以传递的数据为Request,worker将从这里取,engine将种子往这里放 out := make(chan ParseResult) // 种子输出channel,其中可以传递的数据为ParseResult,worker将解析结果传到这里交给engine // 开启若干个worker协程,等待种子被放入 for i := 0; i < e.WorkerCount; i++ { go func() { for { // 开始时所有worker都被阻塞,等待种子被传入 r := <-in // 创建r变量,初始值从输入channel中获取 // 收到某个种子,开始解析 result, e := worker(r) if e != nil { continue } // 将任务执行结果传回engine,但是这里如果直接写out <- result会无法执行,因为有循环等待产生,原因: // 想要执行这行代码必须需要有engine在执行①开始等待才行,但是engine执行这行代码的前提完成②的输入 // 也就是需要有空闲的worker在等待接受in,然而此时worker正在执行本行命令 go func() {out <- result}() } }() } // 传入种子到输入channel for _, request := range seeds { in <- request } for { // 循环结构只有for关键字,这里是当成while(true)使用,让engine一直运行,等待worker返回的数据 ① parseResult := <- out // 现在是打印得到的数据,以后将改为保存到数据库 for _, item := range parseResult.Items { // 相当于foreach,第一个返回值是index,第二个是object,_代表不使用 log.Printf("Got item %v/n", item) } // 存储后将后续种子发送给输入channel for _, r := range parseResult.Requests { ② in <- r } } } func worker(r Request) (ParseResult, error) { log.Printf("Fetching %s", r.Url) // 下载html内容 body, e := fetcher.Fetch(r.Url) if e != nil { log.Printf("Fetcher error with url: %s. %v", r.Url, e) return ParseResult{}, e } // 根据request里提供的解析方法解析当前html内容成为result放入队列 return r.ParserFunc(body), nil }
核心代码就是以上,但还需要实现负责下载的fetcher和负责解析的parser,完整代码可查看 https://github.com/xbox1994/GoCrawler 中的crawler_standalone学习
由于GFW的关系,当你在安装某个依赖包时,可能被挡住,推荐按以下的顺序来尝试安装go的依赖包(xxx代表包的地址) 1. go get xxx
2. 使用gopm安装,先下载gopm: go get -v -u github.com/gpmgo/gopm
,然后 gopm get -g -v xxx
3. 去对应的github主页上, git clone
下来并放到GOROOT或GOPATH目录的src文件夹下,并将下载的文件夹命名为包的地址
Beego是国人开发的一款Go web框架,这里查看相关 简介
它的功能全面、结构清晰、上手容易、有一定社区活跃度但代码质量不高,适合快速开发。
接下来请按照官网 quickstart 的例子来快速体验一把。
在这个例子中要注意的是,Go的方法参数传递只有值传递,没有引用传递,也就是如果传递一个任意类型的变量给某个方法,那么这个变量将被复制一份传给函数内部,所以在函数内部对这个变量进行操作则是对原变量的副本进行操作,如果想直接操作原变量的内容,使用&将地址传进去即可。
本文仅仅是用到了小部分Go的语法和功能特性来尝试带领你入门,还有很多值得了解的比如数组的切片、duck typing 接口、反射,希望你看完之后一定不要止步于此,一切都才刚刚开始,下一步就是开始进行系统的学习了。
这个 知乎回答 可能对你有所帮助,但很不幸给的链接很多都需要翻墙。
https://coding.imooc.com/learn/list/180.html
https://golang.org/doc/faq
https://www.zhihu.com/question/21409296
https://beego.me/quickstart
https://tour.go-zh.org