拥有函数名的函数只能在包级语法块中被声明,通过匿名函数我们可绕过这一限制,在任何表达式中表示一个函数值。更为重要的是,通过这种方式定义的函数可以访问完整的词法环境,这意味着在函数中定义的内部函数可以引用该函数的变量,那么这其实就是一个闭包。
下面我们看一个闭包的例子:
package funcExample
func Square() func()int{
var x int
return func () int {
square := x*x
x++
return square
}
}
package main
import (
"fmt"
"funcExample"
)
func main (){
f := funcExample.Square()
fmt.Println(f()) // 0
fmt.Println(f()) //1
fmt.Println(f()) //4
fmt.Println(f()) //9
fmt.Println(f()) //16
fmt.Println(f()) //25
}
在squares中定义的匿名内部函数可以访问和更新squares中的局部变量,这意味着匿名函数和squares中,存在变量引用。通过这个例子,我们看到变量的生命周期不由它的作用域决定:squares调用结束后,变量x仍然隐式的存在于f中。
接下来的例子,是一个比较有意思的例子,我们将通过深度优先算法结合匿名函数来遍历一个用哈希表模拟的图。
情景如下:
“给定一些计算机课程,每个课程都有前置课程,只有完成了前置课程才可以开始当前课程的学习;我们的目标是选择出一组课程,这组课程必须确保按顺序学习时,能全部被完成”。
我们要做的就是得到一个课程学习列表,让学生能够按正确的次序学习完所有课程。
// prereqs记录了每个课程的前置课程
var prereqs = map[string][]string{
"algorithms": {"data structures"},
"calculus": {"linear algebra"},
"compilers": {
"data structures",
"formal languages",
"computer organization",
},
"data structures": {"discrete math"},
"databases": {"data structures"},
"discrete math": {"intro to programming"},
"formal languages": {"discrete math"},
"networks": {"operating systems"},
"operating systems": {"data structures", "computer organization"},
"programming languages": {"data structures", "computer organization"},
}
这类问题被称作拓扑排序。从概念上说,前置条件可以构成有向图。图中的顶点表示课程,边表示课程间的依赖关系。显然,图中应该无环,这也就是说从某点出发的边,最终不会回到该点。下面的代码用深度优先搜索了整张图,获得了符合要求的课程序列。
package main
import "sort"
var Prereqs = map[string][]string{
"algorithms": {"data structures"},
"calculus": {"linear algebra"},
"compilers": {
"data structures",
"formal languages",
"computer organization",
},
"data structures": {"discrete math"},
"databases": {"data structures"},
"discrete math": {"intro to programming"},
"formal languages": {"discrete math"},
"networks": {"operating systems"},
"operating systems": {"data structures", "computer organization"},
"programming languages": {"data structures", "computer organization"},
}
// 对图进行深度优先遍历,返回一个存储着图中所有节点的切片
func TopSort(graph map[string][]string) (result []string){
// 要用一个map记录已经遍历过的节点
seen := make(map[string]bool)
// 定义一个闭包,用于对图中每一个节点深度遍历
var iter func(node string)
iter = func(node string) {
// 如果该节点遍历过,则跳过
if ok := seen[node]; ok {
return
}
seen[node] = true
neighbors, ok := graph[node]
// 如果一个节点没有相邻节点(体现为这个节点在graph中不存在)则把该节点放入到结果集中
if !ok || len(neighbors) == 0 {
result = append(result, node)
return
}
// 递归遍历node的相邻节点
for _, neighbor := range neighbors {
iter(neighbor)
}
// 递归完node的所有相邻接点后才将node放入到结果集
result = append(result, node)
//return
}
// 在遍历这个图之前,我希望每次调用topSort遍历map时的访问元素顺序是相同的(map元素的遍历是无序的),所以用sort.String给graph的key排一下序
var keys []string
for key := range graph {
keys = append(keys, key)
}
sort.Strings(keys)
// 正式开始遍历这个图
for _, node := range keys {
//seen[node] = true
iter(node)
}
return result
}
func main(){
for _, course := range TopSort(Prereqs) {
fmt.Println(course)
}
}
结果为
intro to programming
discrete math
data structures
algorithms
linear algebra
calculus
formal languages
computer organization
compilers
databases
operating systems
networks
programming languages
当匿名函数需要被递归调用时,我们必须首先声明一个变量(在上面的例子中,我们首先声明了 iter),再将匿名函数赋值给这个变量iter。如果不分成两步,函数字面量无法与iter绑定,我们也无法递归调用该匿名函数。也就是说如果要递归匿名函数 不可以 iter := func (node string) {….}, 只能够 var iter func (node string) 然后 iter = xxxx
上面是我自己参考了作者的写法后重新写的深度优先遍历,下面我们看看作者是怎么写的:
func main() {
for i, course := range topoSort(prereqs) {
fmt.Printf("%d:\t%s\n", i+1, course)
}
}
func topoSort(m map[string][]string) []string {
var order []string
seen := make(map[string]bool)
var visitAll func(items []string)
visitAll = func(items []string) {
for _, item := range items {
if !seen[item] {
seen[item] = true
visitAll(m[item])
order = append(order, item)
}
}
}
var keys []string
for key := range m {
keys = append(keys, key)
}
sort.Strings(keys)
visitAll(keys)
return order
}
作者写的简洁很多。
其实爬取的页面集合就是一个大型的图,爬虫策略其实就是对图进行深度优先或者广度优先的方式来爬取url(每一个url就是一个节点)。
接下来,让我们回到之前的html解析的小项目,现在我希望分别通过深度优先和广度优先的方式遍历一个域名下所有的url。
// 获取一个页面下所有的url
func GetLinks(url string) (urls []string) {
// 请求一个url
//body, err := fetch.FetchBody(url)
resp, err := http.Get(url)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// 解析html
topNode, _ := html.Parse(resp.Body)
resp.Body.Close()
// collectLinks 方法只是遍历节点的前置操作
collectLinks := func (n *html.Node) {
if n.Data == "a" {
for _, attr := range n.Attr {
// 如果这个url不是这个域名下的url,就不添加到urls中
if attr.Key == "href" && !strings.HasPrefix(attr.Val, "javascript"){
var urlInfo *u.URL
var err_parse_url error
fullUrl, _ := resp.Request.URL.Parse(attr.Val) // 获取完整的url
if urlInfo, err_parse_url = u.Parse(fullUrl.String()); err_parse_url == nil && strings.Contains(urlInfo.Host, "zbpblog.com"){
urls = append(urls, fullUrl.String()) // urls是环境变量,会保存状态
}
break
}
}
}
}
// 遍历所有节点,并且记录下节点中的链接到urls中
ForeachNode(topNode, collectLinks, nil)
return urls
}
// 深度优先算法获取一个域名下所有的urls
// startUrls是入口url,可以有多个; 第二参是一个图,要求调用的时候传入一个空图nil
func GetDomainDepthLinks(startUrls []string, graph map[string][]string) []string{
if graph == nil {
graph = map[string][]string {}
}
seenDepth := map[string]bool{}
// 首先我们需要构建一个图
var createGraph func (urls []string, graph map[string][]string)
createGraph = func (urls []string, graph map[string][]string) {
for _, url := range urls {
//url = strings.Trim(url, "/") // 去除首尾两端的/
if _, ok := seenDepth[url]; !ok {
seenDepth[url] = true // 记录访问过的url
innerUrls := getUniqueUrls(GetLinks(url), seenDepth) // GetLinks的返回值去重
fmt.Printf("%s 下所有的url有:%#v \n\n", url, innerUrls)
graph[url] = innerUrls
createGraph(innerUrls, graph)
}
}
}
createGraph(startUrls, graph)
// 构建图之后,再用深度优先算法遍历(或者你可以注释掉下面这行,不在GetDomainDepthLinks中遍历,而是在GetDomainDepthLinks外遍历,GetDomainDepthLinks只负责构建图也可以)
return IterGraphByDepth(graph)
}
func getUniqueUrls(urls []string, seen map[string]bool) (unique_urls []string){
for _, url := range urls {
if _, ok := seen[url]; !ok {
unique_urls = append(unique_urls, url)
}
}
return unique_urls
}
// 遍历graph这个图中所有的表层节点
func IterGraphByDepth(graph map[string][]string) (result []string) {
seen := map[string]bool{}
// 对每一个表层节点进行深度遍历
for startUrl, _ := range graph {
result = parseGraphNode(startUrl, graph, result, seen)
}
return result
}
// 遍历graph这个图中的某一个节点以及该节点的所有相邻节点
// res用于记录一个按深度优先排序的url切片,res中排最前面的url就是整个域名中深度最深的url
// seen用于记录访问过的url,避免重复添加url到res中,因此res中没有重复的url
func parseGraphNode (url string, graph map[string][]string, res []string, seen map[string]bool) []string{
if _, ok := seen[url]; !ok {
seen[url] = true
// 如果这个节点有相邻节点,就先深入查找这些相邻节点
for _, innerUrl := range graph[url] {
res = parseGraphNode(innerUrl, graph, res, seen)
}
// 把所有的相邻节点添加到了res后,才能将自己添加到res中
res = append(res, url)
}
return res
}
func main() {
// 获取一个域名下所有链接,可以有多个入口文件
startUrls := []string {"http://www.zbpblog.com"}
graph := map[string][]string {}
res := parseImprove.GetDomainDepthLinks(startUrls,graph)
fmt.Printf("%#v\n\n\n\n\n\n\n\n", graph)
//将graph存到文件,这样下次想深度遍历这个域名的时候就不用再构建一次图了
graphJson,_ := json.Marshal(graph)
ioutil.WriteFile("1.json",graphJson, 0777)
fmt.Printf("%#v", res)
fmt.Printf("%d", len(res))
}
// 广度优先算法获取一个域名下所有的urls
// workList既是入口urls也是我们想要的存着所有广度优先遍历的urls
func GetDomainWidthLinks(f func(startUrl string) []string, workList []string) (res []string){
seen := map[string]bool{}
loged := map[string]bool{}
for len(workList) > 0 {
for _, url := range workList {
if _, ok := seen[url]; !ok {
//var innerUrls []string
// 将所有innerUrls标记为已记录
innerUrls := f(url)
for _, innerUrl := range innerUrls {
// 如果这个url没有被记录才要记录
if _, ok := loged[innerUrl]; !ok {
fmt.Printf("%s 下的所有未爬取过的url: %s\n", url, innerUrl)
loged[innerUrl] = true
//innerUrls =
workList = append(workList, innerUrl)
}
}
res = append(res, url)
}
}
}
return res
}
func main(){
workList := []string {"http://www.zbpblog.com"}
parseImprove.GetDomainWidthLinks(parseImprove.GetLinks, workList)
fmt.Println(workList)
}
这是我在作者写的函数的基础上作出了一点点修改,加了一个loged去掉将会重复添加到workList中的url。不过这个方法的缺点是即使遍历完所有的url也无法结束,因为for死循环。
可变参数
在参数列表的最后一个参数类型之前加上省略符号“...”,这表示该函数会接收任意数量的该类型参数。
func Add(vals ...int) int {
var sum int
for _, val := range vals {
sum += val
}
return sum
}
vals是一个切片类型,此时Add可以接受0~n个参数。
defer 是go里面的一个关键字,defer会接一个函数调用,作用是做一些延迟操作,比如
func main() {
// ... do something
defer f()
// ... do something
}
当函数f在其所在的函数main执行到defer语句时不会马上执行f调用,而是当f所在的函数(即main函数)执行到return或者发生panic异常的时候才会执行。f的执行发生在f所在函数return时(更准确的说是执行到return语句之后,但真正返回一个值之前的时候),且在f所在函数释放之前执行。
defer语句经常被用于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁。通过defer机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放。
释放资源的defer应该直接跟在请求资源的语句后
例如在网络请求中,我们一般会在读取完响应流的数据之后关闭连接:
func GetUrl(url string) (content []byte, err error){
resp, err := http.Get(url)
if err != nil {
return content, fmt.Errorf("%s get error: %v, not text/html",url, err)
}
defer resp.Body.Close()
// 获取网页的content-type,如果不为 text/html 则直接返回;是text/html的格式才获取响应内容
ct := resp.Header.Get("Content-Type")
if ct != "text/html" && !strings.HasPrefix(ct,"text/html;") {
return content, fmt.Errorf("%s has type %s, not text/html",url, ct)
}
// 将响应流数据写入到content中
content, err = ioutil.ReadAll(resp.Body)
if err != nil {
return content, fmt.Errorf("%s read error: %v", url, err)
}
return content, err
}
在这个例子中,当执行到defer的时候,Close() 不会马上执行,而是当GetUrl执行到return 或者 发生异常的时候才会执行。这样就确保无论是正常退出GetUrl还是因为异常导致GetUrl意外退出,都能够关闭资源。
需要注意 defer 放的位置。defer必须放在资源打开之后的下一句,且是return之前的位置。放在return之前是因为,如果没有执行到defer f() 语句就退出return函数的话,是不会执行f()调用的。
比如上面的例子中,defer resp.Body.Close()
放在 http.Get和第一个if之后而不是之前,这是因为如果http.Get发生错误的话,返回的resp是一个nil,一个nil是无法没有Body成员,也没有Close方法的。
放在resp.Header.Get("Content-Type")和第二个if之前是因为,此时resp不是nil,而是一个网络资源对象,因此可以执行Close了。而且假如放在第二个if之后,而此时请求的是一个图片而不是html页面,就会进入到if代码块执行到第二个return,直接返回空的content和一个自定义的error,但是没有执行到defer f(),那么f自然就不会调用,网络连接就没有成功关闭。
如果我们不用defer也可以做到,但是这个程序需要多处写Close(), 例如:
func GetUrlWithoutDefer(url string) (content []byte, err error){
resp, err := http.Get(url)
if err != nil {
return content, fmt.Errorf("%s get error: %v, not text/html",url, err)
}
// 获取网页的content-type,如果不为 text/html 则直接返回;是text/html的格式才获取响应内容
ct := resp.Header.Get("Content-Type")
if ct != "text/html" && !strings.HasPrefix(ct,"text/html;") {
resp.Body.Close()
return content, fmt.Errorf("%s has type %s, not text/html",url, ct)
}
// 将响应流数据写入到content中
content, err = ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return content, fmt.Errorf("%s read error: %v", url, err)
}
return content, err
}
获取header头失败要写一次,读取完响应流数据之后又要写一次。因此defer可以一定程度简化我们的程序。
在处理其他资源时,也可以采用defer机制,比如对文件的操作
package ioutil
func ReadFile(filename string) ([]byte, error) {
f, err := os.Open(filename)
// #1
if err != nil {
return nil, err
}
defer f.Close() // #2
return ReadAll(f)
}
defer f.Close() 不写在 #1 而是写在 #2 是因为 #1处不确定资源是否已经打开,如果Open打开文件失败的情况下,资源就没有成功打开,f就是一个nil,此时自然无需执行f.Close()关闭资源,而且f是nil根本无法调用Close()。
所以如果放在 #1 并且当Open打开文件失败的情况下,会执行if中的return代码块,然后就会触发defer的f.Close(),然后就会报错说f是nil无法调用Close方法。
除了方便处理资源的打开关闭之外,defer机制还能够用于记录函数的执行情况和信息。例如:
func Run9() {
defer logTime("Run9")()
time.Sleep(3 * time.Second) // Sleep需要传入一个Duration类型,Duration是一个纳秒
}
func logTime(funcName string) func(){
// 记录开始运行时间
startTime := time.Now()
log.Printf("函数 %s 开始运行", funcName)
return func() {
log.Printf("函数 %s 运行结束, 耗时 %s", funcName, time.Since(startTime)) // Since需要传入一个Time对象,返回Duration类型
}
}
这里需要注意的是, defer logTime("Run9")() 应该分为2步,第一步是执行 logTime("Run9") 这个是在运行到defer的时候就会执行的。第二步是执行 logTime(...)(), 这个是要在Run9函数运行结束时才会执行的
log.xxx系列的打印函数比fmt系列的打印函数会多加自动换行和自动输出打印时间。
我们甚至可以使用defer来查看或者改变函数的返回值
func SquareX(x int) (result int){
defer func() { result = result * x } ()
return x * x
}
func main() {
log.Println(funcExample.SquareX(2)) // 8
}
SquareX中的defer定义并执行一个匿名函数,这个匿名函数修改了环境变量result从而直接修改了SquaerX的返回值(return x*x其实会先隐式的将x*x赋值给返回值变量result,再return result。但是在return result之前,defer的匿名函数就将result修改为result = result * x,然后才将result返回出SquareX)。
如果一个函数中有多个defer,那么函数结束时,defer的执行顺序和定义顺序相反。
在循环体中的defer语句需要特别注意,因为只有在函数执行完毕后,这些被延迟的函数才会执行。下面的代码会导致系统的文件描述符耗尽,因为在所有文件都被处理之前,没有文件会被关闭。
for _, filename := range filenames {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close() // NOTE: risky; could run out of file descriptors
// ...对文件标识符的处理
}
除非在循环过程中有某一个文件的打开发生错误,这样就会执行到return err而调用前几次for循环定义的defer
所以我们尽可能少在for循环中使用defer。
一种解决方法是将循环体中的defer语句移至另外一个函数。在每次循环时,调用这个函数。例如:
for _, filename := range filenames {
f, err := os.Open(filename)
if err != nil {
return err
}
func (f *os.File) {
defer f.Close() // close file after handle
// ...对文件标识符的处理 handle
}(f)
}
下面是一个具体的例子
func GetFilesContent(fileNames []string) (res map[string]interface{}){
res = map[string]interface{}{}
for _, fileName := range fileNames {
var (
content []byte
err error
)
if content, err = getFileContent(fileName); err != nil {
res[fileName] = err
continue
}
res[fileName] = string(content)
}
return res
}
func getFileContent (fn string) (c []byte, err error){
f, err := os.Open(fn)
// 不能在这里定义defer f.Close(),因为打开文件可能失败,此时f是一个nil
if err != nil {
return c, err
}
defer f.Close() // 使用了defer下面就可以少写一句f.Close()
fileStat, _ := f.Stat()
c = make([]byte, fileStat.Size()) // 指定 c 的字节长度为文件大小的长度,这样下面Read(c)的时候c才能接收到所有的文件内容,c的字节切片长度是多少就能接收到多少文件内容。
_, err = f.Read(c) // 用c接收文件字节流
return c, err
}
func main(){
fileInfos, err := ioutil.ReadDir("./")
if err != nil {
log.Println(err)
}
fileNames := []string{}
for _, fileInfo := range fileInfos {
if !fileInfo.IsDir(){
fileNames = append(fileNames, fileInfo.Name())
}
}
fileContents := funcExample.GetFilesContent(fileNames)
log.Printf("%#v", fileContents)
}
现在我们改写一下上一节写的Fetch方法,将他改为请求到url之后就写入到文件中。
// 请求一个url并将内容写入到文件中,返回文件的绝对路径
func FetchToFile(url string) (filePath string, err error){
resp, err := http.Get(url)
if err != nil {
return filePath, err
}
defer resp.Body.Close()
// 获取url的文件名
fileName := path.Base(resp.Request.URL.Path)
if fileName == "/"{
fileName = "index.html"
}
// 将数据读到文件
relatePath := "./" + fileName
f, err := os.Create(relatePath)
if err != nil {
return filePath, err
}
_, err = io.Copy(f, resp.Body) // 返回写入文件的字节数
// 关闭文件
if errClose := f.Close(); err == nil {
err = errClose
}
// 如果没有发生错误
filePath,_ = filepath.Abs(relatePath)
return filePath, err
}
这个程序使用了defer resp.Body.Close() 关闭网络连接,但是却没有使用defer机制关闭文件资源,是因为在有些文件系统尤其是NFS,写入文件时发生的错误会被延迟到文件关闭时反馈(也就是说io.Copy的时候如果发生错误不会返回非空的err,而会在f.Close那里才返回写入时的错误err)。如果没有检查文件关闭时的反馈信息,可能会导致数据丢失以及没有将错误返回给调用方(因为此时错误可能由f.Close返回,而我们很可能会认为关闭时不会发生错误因而没有去接受这个错误),而我们还误以为写入操作成功。如果io.Copy和f.close都失败了,我们倾向于将io.Copy的错误信息反馈给调用者,因为它先于f.close发生,更有可能接近问题的本质。
假如我们用defer去关闭文件
_, err = io.Copy(f, resp.Body) // 返回写入文件的字节数
defer f.Close()
filePath,_ = filepath.Abs(relatePath)
return filePath, err
那么f.Close()产生的错误(其实时写入产生的错误,只是被文件系统放到了close中返回)就会被忽略
如果希望使用defer机制关闭文件该怎么做?
// 请求一个url并将内容写入到文件中,返回文件的绝对路径
func FetchToFile(url string) (filePath string, err error){
resp, err := http.Get(url)
if err != nil {
return filePath, err
}
defer resp.Body.Close()
// 获取url的文件名
fileName := path.Base(resp.Request.URL.Path)
if fileName == "/"{
fileName = "index.html"
}
// 将数据读到文件
relatePath := "./" + fileName
f, err := os.Create(relatePath)
if err != nil {
return filePath, err
}
_, err = io.Copy(f, resp.Body) // 返回写入文件的字节数
defer closeFile(f, &err) // 将关闭文件封装起来
filePath,_ = filepath.Abs(relatePath)
return filePath, err
}
func closeFile(f *os.File, err *error) {
// 关闭文件
if errClose := f.Close(); *err == nil {
*err = errClose
}
}
在FetchToFile最终将返回值返回给调用方的时候,closeFile会修改FetchToFile中要return的err
Panic异常和Recover从异常中恢复
当panic异常发生时,程序会中断运行,并立即执行在该goroutine中被延迟的函数(defer 机制)。随后,程序崩溃并输出日志信息。日志信息包括panic value和函数调用的堆栈跟踪信息(就是错误发生在多少行)。
一旦发生Panic异常,无论这个异常是在哪个函数中发生,都会导致整个go程序终止。
除了系统发生报错会返回panic异常之外,自己调用panic函数也会引发panic异常;panic函数接受任何值作为参数。
switch s := suit(drawCard()); s {
case "Spades": // ...
case "Hearts": // ...
case "Diamonds": // ...
case "Clubs": // ...
default:
panic(fmt.Sprintf("invalid suit %q", s)) // Joker?(你在逗我呢吧)
}
Recover函数配合defer机制可以让发生异常的函数不终止整个程序,而是只结束这个函数的运行,并且将panic异常转为error类型的错误,让函数将这个错误返回。(PS:请注意,在go中,panic异常和error错误是两个东西,panic会导致程序终止,而error不会,因此error在go的很多函数或者标准库方法中很常见,会有规范的错误处理机制将error错误返回以供函数调用方自行处理)。
Recover函数一般在defer指定的函数中调用。
通过这种方式,我们就可以避免panic异常导致我们不希望发生的程序终止情况。
下面举一个简单的例子:
package funcExample
import "fmt"
func Run12() (err error){
// 在defer中将Run12从Panic异常中断中恢复,并将这panic信息赋值给err
defer func(){
if p := recover(); p != nil { // p是一个interface{}接口类型,也就是说p可能是任何类型形式的异常
fmt.Printf("%T : %v\n\n",p,p)
err = fmt.Errorf("发生内部错误 : %v", p)
}
}()
a := 0
b := 1
fmt.Println(b/a) // 引发panic异常
fmt.Println("这一行打印不会被执行")
return err
}
func main(){
fmt.Println(funcExample.Run12())
}
在这个例子中,当执行到b/a的时候,会抛出panic异常,然后触发到defer函数。
如果Run12不发生任何异常,defer中的recover函数返回nil,如果Run12发生异常,recover会返回panic异常的内容值,这个内容值一般会是一个字符串。Defer通过recover()捕获这个异常,并将这个异常赋值给环境变量err;如果没有发生异常,那么相当于defer什么都没有做。在这个例子中肯定会发生一个分母不能为0的panic异常。
Defer指定的函数执行之后,函数停止运行,但是程序不会终止。函数中发生报错所在的行之后的内容都不会继续执行(也就是说Run12()在fmt.Println(b/a)之后的语句不会继续执行,而是直接隐式的return函数返回值列表,在这个例子中就是return err。)
因此这个例子的执行结果是
runtime.errorString : runtime error: integer divide by zero
发生内部错误 : runtime error: integer divide by zero
Go的recover结合defer机制就类似于其他语言中的try和catch,可以捕获异常并定制化的处理异常,使得程序不会因为异常的出现而终止。