func name(parameter-list) (result-list) {
body
}
func hypot(x, y float64) float64 {
return math.Sqrt(x*x + y*y)
}
fmt.Println(hypot(3,4)) // "5"
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
fmt.Printf("%T\n", add) // "func(int, int) int"
fmt.Printf("%T\n", sub) // "func(int, int) int"
fmt.Printf("%T\n", first) // "func(int, int) int"
fmt.Printf("%T\n", zero) // "func(int, int) int"
函数的类型被称为函数的标识符。
没有函数体的函数声明,这表示该函数不是以Go实现的。这样的声明定义 了函数标识符。
package math
func Sin(x float64) float //implemented in assembly language
函数可以直接或间接的调用自身。
使用非标准包 golang.org/x/net/html ,解析HTML。
package html
type Node struct {
Type NodeType
Data string
Attr []Attribute
FirstChild, NextSibling *Node
}
type NodeType int32
const (
ErrorNode NodeType = iota
TextNode//文本
DocumentNode
ElementNode
CommentNode//注释
DoctypeNode
)
type Attribute struct {
Key, Val string
}
//读入一组bytes.解析 后,返回html.node类型的HTML页面树状结构根节点。
func Parse(r io.Reader) (*Node, error)
main函数解析HTML标准输入,通过递归函数visit获得links(链接),并打印出这些links:
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)
}
}
visit函数遍历HTML的节点树,从每一个anchor元素的href属性获得link,将这些links存入字符 串数组中,并返回这个字符串数组。
// 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
}
为了遍历结点n的所有后代结点,每次遇到n的孩子结点时,visit递归的调用自身。这些孩子结 点存放在FirstChild链表中。
在函数outline中,我们通过递归的方式遍历整个HTML结点树,并输出树的结构。在outline内 部,每遇到一个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) // push tag
fmt.Println(stack)
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
outline(stack, c)
}
}
大部分HTML页面只需几层递归就能被处理,但仍然有些页面需要 深层次的递归。
Go语言使用可变栈,栈的大小按需增加(初始时很小)。这使得我们使用递归 时不必考虑溢出和安全问题。
log.Println(findLinks(url))
links, err := findLinks(url)
log.Println(links, err)
准确的变量名可以传达函数返回值的含义
func Size(rect image.Rectangle) (width, height int)
func Split(path string) (dir, file string)
func HourMinSec(t time.Time) (hour, minute, second int)
如果一个函数将所有的返回值都显示的变量名,那么该函数的return语句可以省略操作数。
func CountWordsAndImages(url string) (words, images int, err error) {
resp, err := http.Get(url)
if err != nil {
return
}
doc, err := html.Parse(resp.Body)
resp.Body.Close()
if err != nil {
err = fmt.Errorf("parsing HTML: %s", err)
return
}
words, images = countWordsAndImages(doc)
return
}
func countWordsAndImages(n *html.Node) (words, images int) { /* ... */ }
panic是来自被调函数的信号,表示发生了某个已知的bug。
对于那些将运行失败看作是预期结果的函数,它们会返回一个额外的返回值,通常是最后一 个,来传递错误信息。如果导致失败的原因只有一个,额外的返回值可以是一个布尔值,通常被命名为ok。
value, ok := cache.Lookup(key)
if !ok {
// ...cache[key] does not exist...
}
导致失败的原因不止一种,额外的返回值不再是简单的布尔类型,而是error类型。error类型可能是nil或者non-nil。nil意味着函数运行成功,non-nil表示失败。对于non-nil的error类型,我们可以通过调用error的Error函数或者输出函数获得字符串类型的错误信息。
fmt.Println(err)
fmt.Printf("%v", err)
当函数返回non-nil的error时,其他的返回值是未定义的(undefined),这些未定义的返回值应该被忽略。
当读取文件发生错误时,Read函数会返回可以读取的字节数以及错误信息。
函数运行失败时会返回错误信息,这些错误信息被认为是一种预期的值而非异常。
Go使用控制流机制(如if和return)处理异常,这使得编码人员能更多的关注错误处理。
resp, err := http.Get(url)
if err != nil{
return nil, err
}
构造新的错误信息返回给调用者:
doc, err := html.Parse(resp.Body)
resp.Body.Close()
if err != nil {
return nil, fmt.Errorf("parsing %s as HTML: %v", url,err)
}
错误信息经常是以链式组合在一起的,错误信息中应避免大写和换行符。grep的工具处理错误信息
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)
}
if err := WaitForServer(url); err != nil {
fmt.Fprintf(os.Stderr, "Site is down: %v\n", err)
os.Exit(1)
}
调用log.Fatalf:log中的所有函数,都默认会在错误信息之前输出时间信息。
if err := WaitForServer(url); err != nil {
log.Fatalf("Site is down: %v\n", err)
}
我们可以设置log的前缀信息屏蔽时间信息,一般而言,前缀信息会被设置成命令名。
log.SetPrefix("wait: ")
log.SetFlags(0)
通过log包提供函数:log包中的所有函数会为没有换行符的字符串增加换行符。
if err := Ping(); err != nil {
log.Printf("ping failed: %v; networking disabled",err)
}
标准错误流输出错误信息:
if err := Ping(); err != nil {
fmt.Fprintf(os.Stderr, "ping failed: %v; networking disabled\n", 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
io包保证任何由文件结束引起的读取失败都返回同一个错误——io.EOF,该错误在io包中定义:
package io
import "errors"
// EOF is the error returned by Read when no more input is available.
var EOF = errors.New("EOF")
如何从标准输入中读取字符,以及判断文件结束。
in := bufio.NewReader(os.Stdin)
for {
r, _, err := in.ReadRune()
if err == io.EOF {
break // finished reading
}
if err != nil {
return fmt.Errorf("read failed:%v", err)
}
// ...use r...
}
文件结束这种错误不需要更多的描述,所以io.EOF有固定的错误信息——“EOF”。
在Go中,函数被看作第一类值(first-class values)::函数像其他值一样,拥有类型,可以被赋值给其他变量,传递给函数,从函数返回。对函数值(function value)的调用类似函数调用。
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错误
函数值可以与nil比较:
var f func(int) int
if f != nil {
f(3)
}
但是函数值之间是不可比较的,也不能用函数值作为map的key。
strings.Map对字符串中的每个字符调用add1 函数,并将每个add1函数的返回值组成一个新的字符串返回给调用者。
func add1(r rune) rune { return r + 1 }
fmt.Println(strings.Map(add1, "HAL-9000")) // "IBM.:111"
fmt.Println(strings.Map(add1, "VMS")) // "WNT"
fmt.Println(strings.Map(add1, "Admix")) // "Benjy"
拥有函数名的函数只能在包级语法块中被声明,通过函数字面量(function literal),我们可绕过这一限制,在任何表达式中表示一个函数值。
函数值字面量是一种表达式,它的值被成为匿名函数 (anonymous function)。允许我们在使用函数时,再定义它。
strings.Map(func(r rune) rune { return r + 1 }, "HAL-9000")
可以访问完整的词法环境(lexical environment), 这意味着在函数中定义的内部函数可以引用该函数的变量
// 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"
}
函数值属于引用类型和函数值不可比较
闭包(closures)技术、把函数值叫做闭包。
变量的生命周期不由它的作用域决定:squares返回后,变量x仍然隐式的存在于f中。
当匿名函数需要被递归调用时,我们必须首先声明一个变量。再将匿名函数赋值给这个变量。
你被要求首先创建一些目录,再将目录删除
var rmdirs []func()
for _, d := range tempDirs() {
dir := d // NOTE: necessary!
os.MkdirAll(dir, 0755) // creates parent directories too
rmdirs = append(rmdirs, func() {
os.RemoveAll(dir)
})
}
// ...do some work...
for _, rmdir := range rmdirs {
rmdir() // clean up
}
为什么要在循环体中用循环变量d赋值一个新的局部变量
这不是go或 defer本身导致的,而是因为它们都会等待循环结束后,再执行函数值。
参数数量可变的函数称为为可变参数函数。
Printf首先接收一个的必备参数,之后接收任意个数的后续参数。
在声明可变参数函数时,需要在参数列表的最后一个参数类型之前加上省略符号“…”,这表示 该函数会接收任意数量的该类型参数。
func sum(vals...int) int {
total := 0
for _, val := range vals {
total += val
}
return total
}
sum函数返回任意个int型参数的和。在函数体中,vals被看作是类型为[] int的切片。sum可以接 收任意数量的int型参数:
fmt.Println(sum()) // "0"
fmt.Println(sum(3)) // "3"
fmt.Println(sum(1, 2, 3, 4)) // "10"
调用者隐式的创建一个数组,并将原始参数复制到数组中,再把数组的一 个切片作为参数传给被调函数。
如果原始参数已经是切片类型,我们该如何传递给sum?只需 在最后一个参数后加上省略符。
values := []int{1, 2, 3, 4}
fmt.Println(sum(values...)) // "10"
虽然在可变参数函数内部,…int 型参数的行为看起来很像切片类型,但实际上,可变参数函 数和以切片作为参数的函数是不同的。
func f(...int) {}
func g([]int) {}
fmt.Printf("%T\n", f) // "func(...int)"
fmt.Printf("%T\n", g) // "func([]int)"
可变参数函数经常被用于格式化字符串。
errorf函数构造了一个以行号开头的,经过格 式化的错误信息。
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)
}
linenum, name := 12, "count"
errorf(linenum, "undefined: %s", name) // "Line 12: undefined: count"
interfac{}表示函数的最后一个参数可以接收任意类型
你只需要在调用普通函数或方法前加上关键字defer,就完成了defer所需要的语法。当defer语 句被执行时,跟在defer后面的函数会被延迟执行。直到包含该defer语句的函数执行完毕时, defer后的函数才会被执行,不论包含defer语句的函数是通过return正常结束,还是由于panic 导致的异常结束。你可以在一个函数中执行多条defer语句,它们的执行顺序与声明顺序相反。
defer语句经常被用于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁。通 过defer机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放。释放资源的 defer应该直接跟在请求资源的语句后。
文件操作:
package ioutil
func ReadFile(filename string) ([]byte, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.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]
}
调试复杂程序时,defer机制也常被用于记录何时进入和退出函数。
func bigSlowOperation() {
defer trace("bigSlowOperation")() // don't forget the
extra parentheses
// ...lots of work...
time.Sleep(10 * time.Second) // simulate slow
operation by sleeping
}
func trace(msg string) func() {
start := time.Now()
log.Printf("enter %s", msg)
return func() {
log.Printf("exit %s (%s)", msg,time.Since(start))
}
}
不要忘记defer语句后的圆括号,否则本该在进入时执行的操作 会在退出时执行,而本该在退出时执行的,永远不会被执行。
对匿名函数采用defer机 制,可以使其观察函数的返回值。
double函数:
func double(x int) int {
return x + x
}
首先命名double的返回值,再增加defer语句,我们就可以在double每次被调用时,输出参数以及返回值。
func double(x int) (result int) {
defer func() { fmt.Printf("double(%d) = %d\n", x,result) }()
return x + x
}
_ = double(4)
// Output:
// "double(4) = 8"
对于有许多return语句的函数而言, 这个技巧很有用。
在循环体中的defer语句需要特别注意,因为只有在函数执行完毕后,这些被延迟的函数才会执行。
运行时错误会引起painc异常
当panic异常发生时,程序会中断运行,并立即执行在该goroutine中被延迟的函数(defer 机制)。随后,程序崩溃并输出日志信 息。日志信息包括panic value和函数调用的堆栈跟踪信息。panic value通常是某种错误信息。
不是所有的panic异常都来自运行时,直接调用内置的panic函数也会引发panic异常;panic函数接受任何值作为参数。当某些不应该发生的场景发生时,我们就应该调用panic。
由于panic会引起程序的崩溃,因此panic一般用于严重错误,如程序内部的逻辑不一致。
func main() {
f(3)
}
func f(x int) {
fmt.Printf("f(%d)\n", x+0/x) // panics if x == 0
defer fmt.Printf("defer %d\n", x)
f(x - 1)
}
f(3)
f(2)
f(1)
defer 1
defer 2
defer 3
当f(0)被调用时,发生panic异常,之前被延迟执行的的3个fmt.Printf被调用。程序中断执行后,panic信息和堆栈信息会被输出。
runtime包允许程序员输出堆栈信息
在Go的panic机制中,延迟函数的调用在释放堆栈信息之前。
从异常中恢复
如果在deferred函数中调用了内置函数recover,并且定义该defer语句的函数发生了panic异常,recover会使程序从panic中恢复,并返回panic value。导致panic异常的函数不会继续运行,但能正常返回。在未发生panic时调用recover,recover会返回nil。
考虑到语言解析器的复杂性,即使某个语言解析器目前工作正常,也无法肯定它没有漏洞。因此,当某个异常出现时,我们不会选 择让解析器崩溃,而是会将panic异常当作普通的解析错误,并附加额外信息提醒用户报告此错误。
func Parse(input string) (s *Syntax, err error) {
defer func() {
if p := recover(); p != nil {
err = fmt.Errorf("internal error: %v", p)
}
}()
// ...parser...
}
deferred函数帮助Parse从panic中恢复。在deferred函数内部,panic value被附加到错误信息中;并用err变量接收错误信息,返回给调用者。我们也可以通过调用runtime.Stack往错误信 息中添加完整的堆栈调用信息。
不加区分的恢复所有的panic异常,不是可取的做法
不应该试图去恢复其他包引起的panic
有选择性的recover,只恢复应该被恢复的panic异常。这些异常所占的比例应该尽可能的低。为了标识某个panic是否应该被恢复,我们可以将panic value设置成特殊类型。