函数声明
Go中函数的基本形式
func name(parameter-list) (result-list) {
body
}
如果一个函数声明不包括返回值列表,那么函数体执行完毕后,不会返回任何值。如果一个函数在声明时,包含返回值列表,该函数必须以 return语句结尾(否则会报错),除非函数明显无法运行到结尾处。例如函数在结尾时调用了panic异常或函数中存在无限循环。
而且如果声明了返回值列表的话,我们可以直接 return 后面不接东西,它会默认返回返回值列表的变量,例如
func demo1() (r1 int, r2 int){
r1 =1
r2 = 2
return
}
这里会默认返回 r1 和 r2, 这种返回方式叫做 bare return,bare return 可以减少代码的重复,但是使得代码可读性降低,因此不推荐多用。
但是在python或者php中,return后面不接东西会返回None
函数的类型被称为函数的签名。如果两个函数形式参数列表和返回值列表中的变量类型一一对应,那么这两个函数被认为有相同的类型或签名。形参和返回值的变量名不影响函数签名,也不影响它们是否可以以省略参数类型的形式表示。
比如:
func add(x int, y int) int {return x + y}
func sub(x, y int) (z int) { z = x - y; return}
func first(x int, _ int) int { return x }
func zero(int, int) int { return 0 }
这4个函数都有相同的参数列表和返回值列表(参数的个数和类型,返回值的个数和类型都相同),那么这4个函数都是同类型的函数。
Go语言没有默认参数,也没有任何方法可以通过参数名指定形参,因此形参和返回值的变量名对于函数调用者而言没有意义。
没有默认形参很好理解,这意味调用go的函数时,必须有几个形参就要传入几个实参。后半句话的意思是,不支持按照形参的名字传入实参,这意味着传入的实参必须和形参的顺序一一对应。相比与python的函数,例如:
def add(a, b):
return a + b
在调用的时候我们可以这样:
num1 = 10
num2 = 20
print(add(b=num2, a=num1))
通过这种方式我们可以无需记住参数的顺序,直接通过形参名传参即可。
但是Go的函数不支持这个功能,go会说python尽整些花里胡哨的东西。
Go中的函数有几个比较重要的特性
1.参数和返回值在函数调用的时候就会自动声明
这意味着下面这段程序会报错:
func Demo2(p1, p2 string) (r1 string){
r1 := p1 + p2
return r1
}
原因是r1已经声明,无需重复声明。
正确的做法应该是
func Demo2(p1, p2 string) (r1 string){
r1 = p1 + p2
return r1
}
2.参数变量和返回值变量是函数中的局部变量,被初始化为传入的实参值,而且参数变量和返回值变量被存储在相同的词法块中
由于是局部变量,这意味这在函数中对参数变量操作不会影响到外层作用域的变量值,除非传入的参数是引用的类型,例如指针、切片(如果切片添加元素时超过了其最大容量导致扩容则另当别论)、哈希表、函数或者通道(channel)等。
3.函数传参以值的方式传递,因此函数的形参是实参的拷贝而不是引用
如果传入的是一个数组或者结构体的时候,会在底层完全开辟出一块和实参的数组或结构体一样大的空间,然后拷贝数组或者结构体的数据值到这块新的空间里。然后让形参去引用这个新的空间。此时函数外和函数内的这两个变量独立互不影响。
但这样的话会很浪费内存。因此,go官方推荐函数传参不要传入结构体变量本身而是传入结构体变量的指针,用指针一样可以操作结构体,但是这样会影响底层的结构体的内容从而影响到函数外引用该结构体的变量。类似的,通过传入切片代替传入数组,这样做都是为了节省内存。
你可能会偶尔遇到没有函数体的函数声明,这样的声明定义了函数签名。
比如:
func Sin(x float64) float //implemented in assembly language
尤其是在定义一个接口interface的时候
递归
递归就是在函数中调用自己,接触过其他语言的朋友应该也很清楚,其原理是每当函数调用一次自己,就会往一个函数调用栈中入栈这个函数的相关信息,包括函数的上下文环境,局部变量,函数运行到哪里等。最先调用的函数最先入栈最后才出栈。递归的深度越大,这个栈压入的函数信息越多,而且是累加的形式增加,因为后入栈的函数没执行完之前,先调用的函数是不可能先出栈的。因此递归的层数如果太多可能到导致函数调用栈的内存发生溢出(栈一般被创建的时候分配固定的64KB到2MB不等的内存空间,如果入栈的函数太多可能会超过这个空间大小导致栈溢出)。这也是有时候我们看到递归次数太多引发报错的情况。除此之外,还会导致安全性问题。
Go语言使用可变栈,栈的大小按需增加(初始时很小)。这使得我们使用递归时不必考虑溢出和安全问题。
在这一节中,作者使用了一个解析html的例子来演示递归。
目录结构如下
fetch 是我自定义的爬取单页面的包
parse 是解析html的包
main.go 解析来自标准输入的html文本
printHtml.go 爬取url并输出其中的html
# fetch.go
package fetch
import (
"fmt"
"io/ioutil"
"net/http"
"os"
"strconv"
)
// 获取一个url的内容并且打印到标准输出
func FetchPrint(url string) {
content, err := Fetch(url)
// 如果有错误,则将错误输出到标准错误(其实还是输出到屏幕上,但是相比于标准输出的区别是,标准错误没有缓冲区,所以会比标准错误经常会比标准输出要更早打印出来)
// Fprintf作用是将第三参的数据按第二参格式化之后输入到第一参指定的流中,可以是文件流(那就是写入到文件),标准输出流,标准错误流等
if err !=nil {
fmt.Fprintf(os.Stderr, "%s", err)
}
fmt.Fprintf(os.Stdout, "%s", content) // 这里用 fmt.Printf("%s", content) 也可以,因为Printf默认就是输出到标准输出流,也就是打印到屏幕上
}
// 获取一个url的内容并返回响应内容(字符串)
func Fetch(url string) (string, error){
var content string
// 获取一个url的响应, 返回一个Response类型变量
resp, err := http.Get(url)
if err != nil {
// fmt.Errorf 返回一个error错误类型的变量,由于这里要求返回一个error类型所以不能使用Sprintf
return content, fmt.Errorf("获取链接失败: %v", err)
}
//if string(string(resp.StatusCode)[0]) != "2" {
// strconv.Itoa(x int)的作用是将一个int类型的整型转化为字符串型的数字。而 string() 如果传入一个整型会转为一个对应的ASCII
// 所以 正确的数字转字符串的方式是 strconv.Itoa() 而不是 stirng()
// 而且假如a是字符串类型, a[0]不是一个字符串,而是一个uint8, 所以 判断a[0]是否等于字符a不应该用 a[0] == "a" ,而是用 a[0] == 'a' 在Go中双引号是字符串类型,而单引号是rune类型
if strconv.Itoa(resp.StatusCode)[0] != '2' {
return content, fmt.Errorf("获取链接返回状态不正常: %s | %s | %s", resp.Status, string(string(resp.StatusCode)[0]), string(resp.StatusCode)[0])
}
// 如果请求成功,则获取响应流内容并关闭连接响应流(一定是获取了内容之后再关闭哦)
// ioutil.ReadAll通过流的方式读取响应中的字节流内容返回接收到的所有字节流([]byte字节切片类型),需要传入一个io.Reader类型的变量,而resp.Body是io.ReadCloser类型(继承了Reader类型)
b, err := ioutil.ReadAll(resp.Body) // resp的Body成员包括一个可读的服务器响应流
resp.Body.Close() // 关闭resp的Body流(也是关闭连接),防止资源泄露
if err != nil {
return content, fmt.Errorf("io读取响应数据失败: %v", err)
}
content = string(b) // 将字节流转为字符串
return content, nil
}
# parseHtml.go
package parse
import (
"fmt"
"golang.org/x/net/html"
)
/*
我们要通过html.parse(r *io.Reader)函数将一段html字符串转化为html节点对象,parse返回的是一个*html.Node的节点指针类型,而且返回的是html文本中的第一个节点。
func Parse(r io.Reader) (*Node, error)
所以我们可以先看一下这个html.Node节点类型到底包含些什么内容
type Node struct {
Parent, FirstChild, LastChild, PrevSibling, NextSibling *Node
Type NodeType // 节点类型
DataAtom atom.Atom
Data string // 节点数据,如果是一个标签节点,那么这就是标签名
Namespace string
Attr []Attribute // 节点属性
}
节点类型是一个32位的非负整型,一共有7中节点类型,被存到了常量下,
Attr成员是一个html.Attribute类型的切片,Attribute结构体保存一个属性键值对。Attr []Attribute保存的就是一个节点的所有的属性键值对
type Attribute struct {
Namespace, Key, Val string
}
*/
// 遍历一个节点下的所有节点, 接受一个要遍历的节点指针
func Visit(node *html.Node) {
// 如果这个节点是标签节点才显示详细信息
if node.Type == html.ElementNode {
var nodeStr string = "<" + node.Data + " "
// 遍历该节点所有的属性
for _, attr := range node.Attr {
keyValue := attr.Key + "=\"" + attr.Val + "\" "
nodeStr += keyValue
}
nodeStr += ">"
// 打印该节点
fmt.Print(nodeStr + "\n")
}
// 如果这个节点有子节点(有子节点的不一定是标签节点,也可能是错误节点ErrorType或者文档节点DocumentType),那么就递归子节点
if node.FirstChild != nil {
// 递归遍历打印该节点内部的节点
Visit(node.FirstChild)
}
if node.Type == html.ElementNode {
// 打印该节点的结束标签
fmt.Printf("</%s>\n", node.Data)
}
// 如果这个节点有下一个节点,就递归下一个节点
if node.NextSibling != nil {
Visit(node.NextSibling)
}
}
# main.go
package main
import (
"fmt"
"golang.org/x/net/html"
"os"
"parse"
)
func main(){
// 获取屏幕输入的html文本内容来解析
firstNode, err := html.Parse(os.Stdin)
//firstNode, err := html.Parse(strings.NewReader(htmlStr))
if err != nil {
fmt.Fprintf(os.Stdout, "%v", err)
}
// 遍历打印根节点
parse.Visit(firstNode)
}
# printHtml.go
package main
import "fetch"
func main() {
fetch.FetchPrint("http://www.zbpblog.com")
}
在goland调试完毕后,开始编译
cd ./bin && go build main.go && go build printHtml.go
然后我们运行 main.go 和 printHtml.go
./printHtml.exe | ./main.exe
这里使用了管道符,将前一个程序的输出作为后一个程序的输入。
在fetch函数中,我们必须确保resp.Body被关闭,释放网络资源。虽然Go的垃圾回收机制会回收不被使用的内存(用户态的内存),但是这不包括操作系统层面的资源,比如打开的文件、网络连接。因此我们必须显式的释放这些资源。
结果如下
<html lang="zh-CN" >
<head >
</head>
<body >
<title >
</title>
<meta name="keywords" content="张柏沛,个人博客,IT技术博客,PHP教程,Python爬虫,SEO基础知识,前端技术,MySQL教程,人工智能,科技新闻资讯,Web开发,互联网,5G" >
</meta>
<meta name="description" content="张柏沛的IT个人博客,向大家分享自己的php,python和SEO等学习历程和知识,发布有关php/python/seo的技术杂文,展现最新的互联网资讯和智能科技新闻" >
</meta>
<meta charset="UTF-8" >
</meta>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" >
</meta>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" >
</meta>
<link rel="stylesheet" type="text/css" href="/bootstrap/css/bootstrap.min.css" >
</link>
<link rel="stylesheet" type="text/css" href="/statics/css/main.css" >
</link>
<script type="text/javascript" src="/statics/js/jquery.min.js" >
</script>
<script type="text/javascript" src="/bootstrap/js/bootstrap.min.js" >
</script>
<script >
</script>
<script >
</script>
<div class="header-box pc" >
<div class="header-inner" >
<h1 class="header-logo" >
<a href="/" rel="home" >
<img src="/logo.jpg" alt="张柏沛的个人IT技术博客-专注和分享PHP建站和Python技术的学习博客" title="张柏沛的个人IT技术博客-专注和分享PHP建站和Python技术的学习博客" >
</img>
</a>
</h1>
<div class="search-box" >
<form class="search-form" action="/search" method="get" >
<input type="hidden" value="" name="search_type" id="search-type-input" >
</input>
<div class="form-group" >
<select class="form-control" id="search-select" name="search_select" style="border-radius:0" >
<option value="blogs" type="1" >
</option>
<option value="blogs" type="2" >
</option>
<option value="blogs" type="3" >
</option>
<option value="article" type="" >
</option>
</select>
</div>
..... 省略
在后续介绍函数的其他要点时,我们会一步步的将这个程序完善。
多返回值
在go的函数中,允许返回多个值。一般而言,一个函数会返回一个期望得到的值和一个错误信息,就像我们之前看到过的很多标准库的函数的第二个返回值是error类型一样。
返回多个值只需用逗号隔开即可。
调用函数的时候,如果函数返回多个值就必须用多个变量接收,而是接收的变量个数一定要和函数返回的值的个数相同。这一点和python不同,python也支持多返回值的函数,但是python接受函数的多返回值时会显得更灵活,开发者可以接收任意多个返回值,比如python的demo()返回3个返回值,但是可以只接收1个,而Go必须3个都接收。
如果Go也想忽略函数的某些返回值,那么可以用_来占位(但其实这里还是接受了3个返回值,只是忽略了后两个而已):
r1, _, _ = demo()
错误
这里介绍一下go的错误处理,在之后我会还会详细的说错误和异常,但是在这里提到是因为Go的函数总是会大量的处理可能出现的异常,以及Go的函数总是会将error作为返回值返回给调用方。
对于大部分函数而言,永远无法确保能否成功运行。这是因为错误的原因超出了程序员的控制。举个例子,任何进行I/O操作的函数都会面临出现错误的可能,只有没有经验的程序员才会相信读写操作不会失败,即使是简单的读写。因此,当本该可信的操作出乎意料的失败后,我们必须弄清楚导致失败的原因。
如果一个函数可能会发生错误,那么这个函数一般会将错误信息作为最后一个返回值返回(而实际上大部分函数都会这样做,因为大部分函数都不能保证执行函数时不会发生错误)。
如果导致失败的原因只有一个,额外的返回值可以是一个布尔值,通常被命名为ok,比如,cache.Lookup失败的唯一原因是key不存在,那么代码可以按照下面的方式组织
value, ok := cache.Lookup(key)
if !ok {
// ...cache[key] does not exist…
}
如果导致失败的原因可能有多个,尤其是对I/O操作而言,用户需要了解更多的错误信息。因此,额外的返回值不再是简单的布尔类型,而是error类型。
内置的error是接口类型,我们将在之后了解接口类型。现在我们只需要明白error类型可能是nil或者non-nil。nil意味着函数运行成功,non-nil表示失败。
当函数返回non-nil的error时,其他的返回值一般会返回零值。然而,有少部分函数在发生错误时,仍然会返回一些有用的返回值。比如,当读取文件发生错误时,Read函数会返回可以读取的字节数以及错误信息。对于这种情况,正确的处理方式应该是先处理这些不完整的数据,再处理错误。
函数中出现的错误一般不要以异常的形式抛出(不要随意使用log.Fatal这样的抛出错误并终止程序的语句,除非这个错误足够严重到要终止程序),而是使用return返回给调用方,否则会混乱对错误的描述,这通常会导致一些糟糕的后果,比如会增加异常信息的复杂性增加定位错误的难度。
Go中的错误处理一般有以下几种
1.直接返回给调用方
这种处理方式占所有处理方式的80%,如果把错误直接返回给错误方,我们会返回一个error类型而不是string类型的错误。
Go提供了fmt.Errorf(format string, a ...interface{}) 这个函数,他会将错误信息用fmt.Sprintf格式化之后,再封装为error类型返回。
如果我们直接返回一个调用标准库返回的err错误,就不用Errorf这个函数。但是如果我们想除了返回这个err错误之外还附加一些其他信息到这个错误中,我们就可以用Errorf。
例如:
doc, err := html.Parse(resp.Body)
resp.Body.Close()
if err != nil {
return nil, fmt.Errorf("parsing %s as HTML: %v", url, err)
}
这里就用Errorf将url这个信息伴随着err一起返回给客户端了。
凡是像map,数组,结构体这样的复合类型甚至时error类型和其他自定义的复杂类型都可以用 %v 来格式化,用%#v格式化可以显示出更多的信息。
2.重试
如果错误的发生是偶然性的,或由不可预知的问题导致的,可以选择重新尝试失败的操作
在重试时,我们需要限制重试的时间间隔或重试的次数,防止无限制的重试。
例如我想请求1个国外的网页,这个网页不是很稳定,有时候会请求失败,因此如果请求失败就要重复请求。
package funcExample
import (
"fmt"
"io/ioutil"
"net/http"
"time"
)
func Request(url string) (content string, err error){
// 发生错误时重试
resp, err := http.Get(url)
// 正常情况下,如果err不为空而且还要判定具体是哪种错误才能够进行重试,这里只是为了演示,因此只要发生错误就重试
if err != nil {
// 定义重试的时间范围是1分钟,1分钟内请求失败会重复请求
scope := time.Minute // 是一个Duration对象,单位是纳秒
dealine := time.Now().Add(scope) // 这是1分钟后的Time时间对象
// 如果超过一分钟就会结束循环
for tries := 0; time.Now().Before(dealine); tries++ {
fmt.Printf("重试请求 %s \n", url)
resp, err = http.Get(url) // 不要用 :=,因为这里的err要覆盖上面的err才行
// 重试失败就再重试
if err != nil {
// 睡一段时间,睡的时间随着重试次数增加而加长
time.Sleep(time.Second << tries)
continue
}
// 重试成功
break
}
}
// 如果重试1分钟还是失败则返回错误
if err != nil {
return content, fmt.Errorf("请求url %s 失败: %v", url, err)
}
// 读取失败
var res_bytes []byte
res_bytes, err = ioutil.ReadAll(resp.Body)
if err != nil{
return string(res_bytes), fmt.Errorf("读取url %s 的响应失败: %v", url, err)
}
return string(res_bytes), err
}
3.输出错误信息并结束程序
如果错误发生后,程序无法继续运行,我们就可以采用第三种策略:输出错误信息并结束程序
需要注意的是,这种策略只应在main中执行。对库函数而言,应仅向上传播错误(即直接return错误给调用方),除非该错误意味着程序内部包含不一致性,即遇到了bug,才能在库函数中结束程序。
// (In function main.)
if err := WaitForServer(url); err != nil {
fmt.Fprintf(os.Stderr, "Site is down: %v\n", err)
os.Exit(1)
}
调用log.Fatalf可以更简洁的代码达到与上文相同的效果。log中的所有函数,都默认会在打印信息之前输出时间信息。
log.Fatalf("Site is down: %v\n", err)
我们可以设置log的前缀信息屏蔽时间信息,一般而言,前缀信息会被设置成命令名
log.SetPrefix("wait: ")
log.SetFlags(0)
4.只打印错误信息
可以使用log.Printf 或者 fmt.Printf 或者 fmt.Fprintf
log.Printf("ping failed: %v; networking disabled",err)
fmt.Printf("ping failed: %v; networking disabled",err)
fmt.Fprintf(os.Stderr, "ping failed: %v; networking disabled\n", err)
其中前二者的区别是:log包中的所有函数会为没有换行符的字符串增加换行符。
5.直接忽略掉错误
==========================
在Go中,错误处理有一套独特的编码风格。检查某个子函数是否失败后,我们通常将处理失败的逻辑代码放在处理成功的代码之前(而且一般错误的处理会放在if中并返回,而成功的处理不会放在else中而是在if的代码块之后和之外)。如果某个错误会导致函数返回,那么成功时的逻辑代码不应放在else语句块中,而应直接放在函数体中。
例如:
res, err := http.Get(url)
if err != nil {
// 错误逻辑的处理
return fmt.Errorf(...)
}
// 没有错误时的逻辑处理
// ...
文件结尾错误(EOF)
当我们从文件中读取n个字节。如果n等于文件的长度,读取过程的任何错误都表示失败。如果n小于文件的长度,调用者会重复的读取固定大小的数据直到文件结束。这会导致调用者必须分别处理由文件结束引起的各种错误。基于这样的原因,io包保证任何由文件结束引起的读取失败(就是指针指到文本尾部)都返回同一个错误——io.EOF
in := bufio.NewReader(os.Stdin) // 创建一个标准输入的文件流对象
for { // 死循环
r, _, err := in.ReadRune() // 每次从标准输入中读取1个字符
if err == io.EOF { // 如果文件指针到达最后一个字符则跳出循环
break // finished reading
}
if err != nil {
return fmt.Errorf("read failed:%v", err)
}
// ...use r…
}