函数可以让我们将一个语句序列打包为一个单元,然后可以从程序中其它地方多次调用。函数的机制可以让我们将一个大的工作分解为小的任务,这样的小任务可以让不同程序员在不同时间、不同地方独立完成。一个函数同时对用户隐藏了其实现细节。
函数声明
函数声明包括函数名、形式参数列表、返回值列表(可省略)以及函数体。
func name(parameter-list) (result-list) {
body
}
函数类型
函数的类型被称为函数的标识符。如果两个函数形式参数列表和返回值列表中的变量类型一一对应,那么这两个函数被认为有相同的类型和标识符。形参和返回值的变量名不影响函数标识符也不影响它们是否可以以省略参数类型的形式表示。
在函数体中,函数的形参作为局部变量,被初始化为调用者提供的值。实参通过值的方式传递,因此函数的形参是实参的拷贝。对形参进行修改不会影响实参。但是,如果实 参包括引用类型,如指针,slice(切片)、map、function、channel等类型,实参可能会由于函数的简介 引用被修改。
递归
函数可以是递归的,这意味着函数可以直接或间接的调用自身。
// visit appends to links each link found in n and returns the result.
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
}
多返回值
在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
}
调用多返回值函数时,返回给调用者的是一组值,调用者必须显式的将这些值分配给变量
links, err := findLinks(url)
如果某个值不被使用,可以将其分配给blank identifier
links, _ := findLinks(url) // errors ignored
错误
在Go的错误处理中,错误是软件包API和应用程序用户界面的一个重要组成部分,程序运行失败仅被认为 是几个预期的结果之一。
对于那些将运行失败看作是预期结果的函数,它们会返回一个额外的返回值,通常是最后一个,来传递错误信息。如果导致失败的原因只有一个,额外的返回值可以是一个布尔值,通常被命名为ok。
error类型可能是nil或者non-nil。nil意味着函数运行成功,non-nil表示失败。
错误处理策略
- 最常用的方式是传播错误
resp, err := http.Get(url)
if err != nil{
return nill, err
}
- 如果错误的发生是偶然性的,或由不可预知的问题导致的。一个明智的选择是重新尝试失败的操作。在重试时,我们需要限制重试的时间间隔或重试的次数,防止无限制的重试。
func WaitForServer(url string) error {
const timeout = 1 * time.Minute
deadline := time.Now().Add(timeout)
for tries := 0; time.Now().Before(deadline); tries++ {
_, err := http.Head(url)
if err == nil {
return nil // success
}
log.Printf("server not responding (%s);retrying...", err)
time.Sleep(time.Second << uint(tries)) // exponential back-off
}
return fmt.Errorf("server %s failed to respond after %s", url, timeout)
}
- 如果错误发生后,程序无法继续运行,我们就可以采用第三种策略:输出错误信息并结束程序。
// (In function main.)
if err := WaitForServer(url); err != nil {
fmt.Fprintf(os.Stderr, "Site is down: %v\n", err)
os.Exit(1)
}
- 有时,我们只需要输出错误信息就足够了,不需要中断程序的运行。
if err := Ping(); err != nil {
log.Printf("ping failed: %v; networking disabled",err)
}
- 最后一种策略:我们可以直接忽略掉错误。
dir, err := ioutil.TempDir("", "scratch")
if err != nil {
return fmt.Errorf("failed to create temp dir: %v",err)
}
// ...use temp dir...
os.RemoveAll(dir) // ignore errors; $TMPDIR is cleaned periodically
函数值
在Go中,函数被看作第一类值(first-class values):函数像其他值一样,拥有类型,可以被赋值给 其他变量,传递给函数,从函数返回。对函数值(function value)的调用类似函数调用。
函数值之间是不可比较的,也不能用函数值作为map的key。
func square(n int) int { return n * n }
func negative(n int) int { return -n }
func product(m, n int) int { return m * n }
f := square
fmt.Println(f(3)) // "9"
f = negative
fmt.Println(f(3)) // "-3"
fmt.Printf("%T\n", f) // "func(int) int"
f = product // compile error: can't assign func(int, int) int to func(int) int
函数类型的零值是nil。调用值为nil的函数值会引起panic错误:
var f func(int) int
f(3) // 此处f的值为nil, 会引起panic错误
匿名函数
拥有函数名的函数只能在包级语法块中被声明,通过函数字面量(function literal),我们可绕过这 一限制,在任何表达式中表示一个函数值。函数字面量的语法和函数声明相似,区别在于func关键字后 没有函数名。函数值字面量是一种表达式,它的值被成为匿名函数(anonymous function)。
// squares返回一个匿名函数。
// 该匿名函数每次被调用时都会返回下一个数的平方。 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"
}
可变参数
参数数量可变的函数称为为可变参数函数。典型的例子就是fmt.Printf和类似函数。Printf首先接收一 个的必备参数,之后接收任意个数的后续参数。
在声明可变参数函数时,需要在参数列表的最后一个参数类型之前加上省略符号“...”,这表示该函数 会接收任意数量的该类型参数。
func sum(vals...int) int {
total := 0
for _, val := range vals {
total += val
}
return total
}
Deferred函数
defer语句经常被用于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁。通过defer机 制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放。释放资源的defer应该直接跟在请 求资源的语句后。
func title(url string) error {
resp, err := http.Get(url)
if err != nil {
return err }
defer resp.Body.Close()
ct := resp.Header.Get("Content-Type")
if ct != "text/html" && !strings.HasPrefix(ct,"text/html;") {
return fmt.Errorf("%s has type %s, not text/html",url, ct)
}
doc, err := html.Parse(resp.Body)
if err != nil {
return fmt.Errorf("parsing %s as HTML: %v", url,err)
}
// ...print doc's title element...
return nil
}
Panic异常
Go的类型系统会在编译时捕获很多错误,但有些错误只能在运行时检查,如数组访问越界、空指针引用 等。这些运行时错误会引起painc异常。
一般而言,当panic异常发生时,程序会中断运行,并立即执行在该goroutine(可以先理解成线程,在 第8章会详细介绍)中被延迟的函数(defer 机制)。随后,程序崩溃并输出日志信息。日志信息包括 panic value和函数调用的堆栈跟踪信息。panic value通常是某种错误信息。
直接调用内置的panic函数:
switch s := suit(drawCard()); s {
case "Spades":
case "Hearts":
case "Diamonds":
case "Clubs":
default:
panic(fmt.Sprintf("invalid suit %q", s)) // Joker?
}
Recover捕获异常
通常来说,不应该对panic异常做任何处理,但有时,也许我们可以从异常中恢复,至少我们可以在程序 崩溃前,做一些操作。
func Parse(input string) (s *Syntax, err error) {
defer func() {
if p := recover(); p != nil {
err = fmt.Errorf("internal error: %v", p)
}
}()
// ...parser...
}
作为被广泛遵 守的规范,你不应该试图去恢复其他包引起的panic。公有的API应该将函数的运行失败作为error返回, 而不是panic。
网友评论