函数
函数声明
func name(parameter-list) (result-list) { body }
- 函数声明包含函数名、形参列表、可选的返回列表以及函数体 。形参列表指定由调用者传递的变量参数名和类型;返回列表指定函数返回值类型 ,无返回值或返回未命名值时,返回列表括号可省略 。
func hypot(x, y float64) float64 {
return math.Sqrt(x * x + y * y)
}
hypot
函数:func hypot(x, y float64) float64
,x
和y
是形参,调用时如hypot(3, 4)
,3
和4
是实参 ,函数返回float64
类型值 。
func f(i, j, k int, s, t string)
func f(i int, j int, k int, s string, t string)
- 形参和返回值的简写:若多个形参或返回值类型相同,类型只需写一次 ,上述两种情况等价 。
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 }
- 函数声明的多种方式:声明带两个形参和一个返回值(变量均为
int
类型 )的 4 种方式 ,包括常规写法和形参未使用时用空白标识符的写法 。
类型与签名
- 函数的形参列表和返回列表构成函数签名 ,当两个函数形参列表和返回列表相同,就认为它们类型或签名相同 ,形参和返回值名字、简写方式不影响函数类型 。
调用规则
- 调用函数需按顺序提供与形参对应的实参 ,Go 语言无默认参数值,不能指定实参名 。形参是函数最外层局部变量,初始值由实参传递 ,函数形参和命名返回值变量属函数局部作用域 。实参按值传递,函数接收实参副本 ,但当实参为引用类型(指针、slice、map 等 )时,函数可能间接修改实参变量 。
特殊的函数声明
- 有些函数声明无函数体,意味着该函数用 Go 以外语言实现 ,如
package math
中的func Sin(x float64) float64
,Sin
函数使用汇编语言实现 。
递归
- 函数可递归调用,即直接或间接调用自身 ,能处理具有递归特性的数据结构 。
package main
import (
"fmt"
"os"
"golang.org/x/net/html"
)
func main() {
doc, err := html.Parse(os.Stdin)
if err != nil {
fmt.Fprintf(os.Stderr, "findlinks1: %v\n", err)
os.Exit(1)
}
for _, link := range visit(nil, doc) {
fmt.Println(link)
}
}
func visit(links []string, n *html.Node) []string {
if n.Type == html.ElementNode && n.Data == "a" {
for _, a := range n.Attr {
if a.Key == "href" {
links = append(links, a.Val)
}
}
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
links = visit(links, c)
}
return links
}
findlinks1
程序:查找 HTML 超链接的递归实现。主函数从标准输入读入 HTML ,调用递归函数visit
获取超链接 。visit
函数遍历 HTML 树节点,当节点是元素节点且有a
标签属性时,提取href
属性值添加到字符串 slice ,并递归访问子节点 ,最后输出找到的所有超链接 。可将fetch
程序输出定向到findlinks1
,获取网页超链接 。使用:
go build .\ch1\fetch\
go build .\ch5\findlinks1\
.\fetch.exe https://baidu.com | .\findlinks1.exe
http://news.baidu.com
http://www.hao123.com
http://map.baidu.com
http://v.baidu.com
http://tieba.baidu.com
//www.baidu.com/more/
http://home.baidu.com
http://ir.baidu.com
http://www.baidu.com/duty/
http://jianyi.baidu.com/
package main
import (
"fmt"
"os"
"golang.org/x/net/html"
)
func main() {
doc, err := html.Parse(os.Stdin)
if err != nil {
fmt.Fprintf(os.Stderr, "outline: %v\n", err)
os.Exit(1)
}
outline(nil, doc)
}
func outline(stack []string, n *html.Node) {
if n.Type == html.ElementNode {
stack = append(stack, n.Data)
fmt.Println(stack)
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
outline(stack, c)
}
}
outline
程序:输出 HTML 文档结构的递归实现。使用递归遍历 HTML 文本节点树并输出结构 。outline
函数遇到元素节点时将标签压入栈并输出栈内容,然后递归访问子节点 。递归过程中栈的副本传递,被调函数对栈操作不影响调用者原栈 。通过该程序可查看如https://baidu.com
页面的 HTML 结构 。
递归栈
- 许多编程语言栈长度固定(64KB - 2MB ),深度递归可能栈溢出 。Go 语言使用可变长度栈,上限可达 1GB 左右 ,能安全使用递归 。
多返回值
- Go 语言中函数可返回多个结果 ,常见于标准包内函数返回计算结果与错误值、表示函数调用是否正确的布尔值等情况 。
func findlinks(url string) ([]string, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
return nil, fmt.Errorf("getting %s: %s", url, resp.Status)
}
doc, err := html.Parse(resp.Body)
resp.Body.Close()
if err != nil {
return nil, fmt.Errorf("parsing %s as HTML: %v", url, err)
}
return visit(nil, doc), nil
}
findLinks
函数:findLinks
函数发起 HTTP GET 请求,解析返回的 HTML 页面并返回所有链接 ,声明为func findLinks(url string) ([]string, error)
。- 实现:函数中先发起请求,若请求出错直接返回错误;若状态码非
http.StatusOK
,关闭响应体并返回错误 ;接着解析 HTML ,若解析出错也返回错误 ;成功时返回链接 slice 和空错误值 。要确保响应体resp.Body
正确关闭以释放网络资源 。
调用与处理
变量赋值:调用多返回值函数时,调用者需显式将返回值赋给变量 ,如
links, err := findLinks(url)
,也可忽略某个返回值,用空白标识符_
,如links, _ := findLinks(url)
。嵌套调用:多返回值可用于调用另一个多值返回的函数 ,如
findLinksLog
函数在findLinks
基础上增加记录参数的动作 。多值调用还可作为实参传递给有多个形参的函数 ,方便调试 。命名返回值:函数可定义命名返回值 ,这些变量在函数内初始化为对应类型零值 ,可省略
return
语句操作数 ,称为裸返回 。裸返回虽能减少代码重复,却可能使代码可读性变差,应谨慎使用 。
错误
- 有些函数总是能成功返回结果,如
strings.Contains
和strconv.FormatBool
,对所有可能参数都有定义好的返回 ,即使存在内存耗尽等极端情况,其错误表现和起因复杂且恢复渺茫 。 - 部分函数符合前置条件就能成功返回,如
time.Date
,但参数不当会导致宕机 。 - 还有很多函数受外部因素影响,不能保证一定成功返回,如 I/O 操作函数 ,这些地方是错误处理的重点 。
返回方式
- 函数调用发生错误时,习惯将错误值作为最后一个结果返回 。若错误情况单一,结果常设为布尔类型 ,如
cache.Lookup
,成功返回值和true
,键不存在返回false
。对于 I/O 等操作,错误原因多样,返回类型常为error
。
error 类型
error
是内置接口类型 ,一个错误可能为空值(表示成功 )或非空值(表示失败 ) ,非空错误类型有错误消息字符串 ,可通过Error
方法或fmt.Println
、fmt.Printf
输出错误消息 。
错误处理原则
- 当函数返回非空错误时,其他结果通常无定义应忽略 ,但有些函数出错时会返回部分可用结果 ,调用者应先处理错误 。Go 语言通过普通返回值报告错误 ,而非异常机制(Go 语言异常仅针对程序 bug 导致的预料外错误 ) ,使用常规控制流(如
if
和return
)处理错误 ,虽要求更谨慎,但这是设计要点 。
错误处理策略
- 传递错误
当函数调用返回错误时,将错误直接传递给调用者 。如findLi nks
函数中,http.Get
失败时直接返回错误 ;html.Parse
失败时,构建包含相关信息的新错误消息再返回 。设计错误消息要慎重,包含充足相关信息且保持一致 ,像os
包的文件操作函数返回的错误就包含文件名字等信息 。
- 重试操作
对于不固定或不可预测的错误,在短时间间隔后重试 。如WaitForServer
函数尝试连接 URL 对应的服务器 ,设置超时时间,在规定时间内多次重试 ,超过重试次数和限定时间后报错退出 。
- 输出错误并停止程序
若错误无法顺利解决,输出错误信息后优雅停止程序 。一般库函数将错误传递给调用者,主程序部分可处理错误并停止 ,可使用log.Fatalf
实现,也可自定义日志输出格式 。
- 记录错误并继续运行
在一些错误情况下,仅记录错误信息,程序继续运行 。可使用log
包记录日志,也可直接输出到标准错误流 。
- 忽略错误
在某些特殊情况(如操作系统会周期性清理临时目录 )下,直接忽略错误 。但有意忽略错误时要清楚逻辑后果,函数错误处理通常在开头检查并返回错误,再执行实际函数体 。
文件结束标识
当从文件读取数据时,若要读取的字节数为文件长度,任何错误都代表操作失败 ;若反复读取固定大小数据块直到文件耗尽,就需区分读取到文件尾和遇到其他错误的情况 。
io.EOF
的定义
io
包中定义了io.EOF
,用于表示由文件结束引起的读取错误 ,其定义为var EOF = errors.New("EOF")
,即当没有更多输入时返回 。
io.EOF
的使用
in := bufio.NewReader(os.Stdin)
for {
r, _, err := in.ReadRune()
if err == io.EOF {
break
}
if err != nil {
return fmt.Errorf("read failed: %v", err)
}
// ...使用r
}
- 使用
bufio.Reader
的ReadRune
方法读取 ,当err == io.EOF
时表示读取到文件结束 ,退出循环 ;若err != nil
则表示其他错误 ,返回错误信息 。io.EOF
有固定错误消息 ,而对于其他错误 ,可能需要更多错误本质原因和数量信息 。
函数变量
func square(n int) int { return n * n }
f := squeare
fmt.Println(f(3))
函数在 Go 语言中是头等重要的值 ,函数变量有类型 ,可赋值给变量、传递或从其他函数返回 ,也能像普通函数一样调用 。函数类型零值是nil
,调用nil
函数会导致宕机 ,函数变量可与nil
比较,但函数变量本身不可相互比较,也不能作为map
键值 。
func add1(r rune) rune { return r + 1 }
fmt.Println(strings.Map(add1, "Admix")) // "Benjy"
以strings.Map
为例,它对字符串每个字符应用一个函数并连接结果 ,如add1
函数配合strings.Map
实现对字符串中字符的操作 。
匿名函数
- 命名函数在包级别作用域声明,匿名函数则可在表达式内定义 ,无函数名称 ,像函数字面量 。如
strings.Map
调用时可直接定义匿名函数 。
func squares() func() int {
var x int
return func() int {
x++
return x * x
}
}
func main() {
f := squares()
fmt.Println(f()) // 1
fmt.Println(f()) // 4
fmt.Println(f()) // 9
fmt.Println(f()) // 16
}
- 匿名函数能获取整个词法环境 ,以
squares
函数为例,它返回一个匿名函数 ,该匿名函数可引用并修改外层函数的局部变量x
,每次调用返回x
的平方并递增x
,体现了函数变量可拥有状态 ,这种函数类型称为引用类型 ,且函数变量因闭包原因无法比较 。
警告:捕获迭代变量
var rmdirs []func() for _, dir := range tempDirs() { dir := dir // 声明内部 dir,并以外部dir初始化 os.MkdirAll(dir, 07555) rmdirs = append(rmdirs, func() { os.RemoveAll(dir) }) } // ...处理 for _, rmdir := range rmdirs { rmdir() // 清理 }
问题示例
以创建并删除一系列目录为例 ,代码中使用包含函数变量的 slice 进行清理操作 。若在
for - range
循环中直接使用循环变量dir
创建匿名函数 ,会出现问题 。因为循环变量dir
在循环引入的块作用域内声明 ,循环内创建的所有函数变量共享该变量的存储位置 ,其值在迭代中不断更新 。当调用清理函数时,dir
已被更新多次 ,实际取值是最后一次迭代的值 ,导致所有os.RemoveAll
调用都试图删除同一个目录 。解决方法
为避免此问题,可在循环内引入一个新的局部变量 ,将循环变量赋值给它 ,如
dir := dir
,相当于创建一个副本 ,这样每个匿名函数捕获的是不同的变量值 。
变长函数
func sum(vals ...int) int {
total := 0
for _, val := range vals {
total += val
}
return total
}
变长函数在调用时可接受可变数量的参数 ,在参数列表最后的类型名称前使用省略号 “…” 声明 。如func sum(vals ...int) int
,在函数体内vals
是int
类型的 slice ,可接收任意数量的int
参数 。
调用方式
- 可直接传递多个参数调用 ,如
sum(1, 2, 3, 4)
。 - 当实参已存在于 slice 中时,在最后一个参数后面放省略号调用 ,如
values := []int{1, 2, 3, 4}; sum(values...)
。变长函数类型与带普通 slice 参数的函数类型不同 。
func errorf(linenum int, format string, args ..interface{}) {
fmt.Fprintf(os.Stderr, "Line %d: ", linenum)
fmt.Fprintf(os.Stderr, format, args...)
fmt.Fprintln(os.Stderr)
}
常用于格式化字符串 ,以errorf
函数为例 ,它构建格式化错误消息 ,第一个参数为行号 ,第二个参数为格式字符串 ,后面可变参数为interface{}
类型 ,可接受任意值 。
延迟函数调用
func title(url string) error {
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close();
// ...
return nil
}
在处理网络请求和资源释放时 ,存在重复调用resp.Body.Close()
来关闭网络连接的情况 。随着函数逻辑变复杂 ,这种重复操作会带来维护问题 ,defer
机制可简化此类工作 。
- 语法:
defer
语句是在普通函数或方法调用前加上defer
关键字 ,函数和参数表达式在语句执行时求值 。 - 执行规则:无论函数正常执行完(遇到
return
语句 )还是发生宕机等异常情况 ,defer
调用都推迟到函数结束时执行 ,且按调用defer
语句顺序的倒序执行 ,defer
语句使用次数无限制 。
应用场景
func ReadFile(filename string) ([]btye, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer.Close()
return ReadAll(f)
}
var mu sync.Mutex
var m = make(map[string]int)
func lookup(key string) int {
mu.lock()
defer mu.unlock()
return m[key]
}
- 资源管理:用于成对操作 ,如打开和关闭文件(
io/ioutil
包中ReadFile
函数使用defer
关闭文件 )、连接和断开网络连接(title
函数中用defer resp.Body.Close()
关闭响应体 )、加锁和解锁(sync.Mutex
相关操作 )等 ,确保资源在任何情况下都能正确释放 。
func bigSlowOperation() {
defer trace("bigSlowOperation")()
// ...
timeSleep(10 * time.Second)
}
func trace(msg string) func() {
start := time.Now()
log.Printf("enter %s", msg)
return func { log. Printf("exit %s (%s)", msg, time.Since(start)) }
}
- 函数调试:在复杂函数 “入口” 和 “出口” 设置调试行为 ,如
bigSlowOperation
函数配合trace
函数 ,记录进入和退出函数的时间及时间差 。
func double(x int) (result int) {
defer func() { fmt.Printf("double(%d) = %d\n", x, result) }()
return x + x
}
func triple(x int) (result int) {
defer func() { result += x }
return double(x)
}
- 修改返回结果:延迟执行的匿名函数可更新函数结果变量 ,如
double
和triple
函数通过defer
语句在返回前输出参数和结果 ,或改变返回给调用者的结果 。
注意事项
for _, filename := range filenames {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close() // 注意:可能会用尽文件描述符
// ...
}
// 解决方法
for _, filename := range filenames {
if err := doFile(filename); err != nil {
return err
}
}
func doFile(filename string) error {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close()
// ...
}
在循环中使用defer
语句要注意 ,若处理完后没关闭文件 ,可能导致资源未正确释放 ,可将循环体(含defer
语句 )放到另一个函数中解决 。
宕机
Go 语言类型系统捕获编译时错误,但运行时数组越界、空指针引用等错误会引发宕机 。宕机发生时,正常程序执行终止,goroutine 中延迟函数执行,程序异常退出并留下日志消息 ,包含宕机值和函数调用栈跟踪信息,可辅助诊断问题 。
主动触发
- 可直接调用内置的
panic
函数触发宕机 ,如在逻辑上不可能到达的分支 ,或遇到 “不可能发生” 的状况时 。但设置断言检查时需谨慎 ,若无法提供有效错误消息或快速检测错误 ,运行时断言检查无意义 。
使用场景
- 仅在发生严重错误,如与预期逻辑不一致时使用宕机 ,稳健代码应优先处理 “预期的” 错误(如错误输入、配置或 I/O 失败 ) ,通过返回错误值区分 。
func Compile(expr string) (*Regexp, error) { /*...*/ }
func MustCompile(expr string) *Regexp {
re, err := Compile(expr)
if err != nil {
panic(err)
}
return re
}
- 以
regexp
包为例,regexp.MustCompile
是包装函数 ,在编译正则表达式出错时触发宕机 ,方便初始化包级别的正则表达式变量 ,但不应接收不正确的值 。
宕机时延迟函数的执行
- 宕机发生时,所有延迟函数按倒序执行 ,从栈顶函数开始返回至
main
函数 ,通过示例展示了延迟函数在宕机前后的执行顺序 。
宕机状态恢复
runtime
包提供runtime.Stack
等方法 ,可输出函数栈信息 ,辅助诊断错误 ,且函数可从宕机状态恢复至正常运行状态避免程序退出 。
恢复
通常退出程序是处理宕机的方式,但在某些情况下可进行恢复 ,如 Web 服务器遇到未知错误时 ,可先清理状态再汇报错误 。recover
函数在延迟函数内部调用 ,能终止当前宕机状态并返回宕机值 ,若在非延迟函数中调用或无宕机发生则返回nil
。
func Parse(input string) (s *Syntax, err error) {
defer func() {
if p := recover(); p != nil {
err = fmt.Errorf("internal error: %v", p)
}
}()
// ...
}
- 以
Parse
函数为例 ,在延迟函数中使用recover
从宕机状态恢复 ,利用宕机值组成错误消息 ,并可结合runtime.Stack
包含调用栈信息 ,将错误赋给结果变量返回给调用者 。
func soleTitle(doc *html.Node) (title string, err error) {
type bailout struct{}
defer func() {
switch p := recover(); p {
case nil:
// 没有宕机
case bailout{}:
// "预期的"宕机
err = fmt.Errorf("multiple title elements")
default:
panic(p) // 未知宕机;继续宕机
}
}()
forEachNode(doc, func(n *html.Node)) {
if n.Type == html.ElementNode && n.Data == "title" &&
n.FirstChild != nil {
if title != "" {
panic(bailout{})
}
title = n.FirstChild.Data
}
}, nil)
if title == "" {
return "", fmt.Errorf("no title element")
}
return title, nil
}
- 对于
soleTitle
函数 ,处理 HTML 文档标题时 ,若文档含多个<title>
元素会触发宕机 ,通过延迟函数调用recover
,检查宕机值 。若为特定类型bailout
,返回普通错误;若为其他非空值 ,说明是预料外宕机 ,继续宕机过程 。
原则
- 无差别恢复不可靠 ,因为宕机后包内变量状态可能不明确 ,可能存在数据结构更新错误、资源未关闭或未释放等问题 。
- 一般不应尝试恢复来自其他包或非自己维护代码中的宕机 ,公共 API 应直接报告错误 。如
net/http
包的 Web 服务器 ,虽可使用recover
输出栈跟踪信息后继续工作 ,但存在资源泄露等风险 。 - 最安全做法是选择性使用
recover
,可通过明确的非导出类型作为宕机值 ,检测recover
返回值是否为此类型来处理宕机 ,不是则继续触发宕机 。
参考资料:《Go程序设计语言》