Go:函数

发布于:2025-04-14 ⋅ 阅读:(25) ⋅ 点赞:(0)

函数

函数声明

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) float64xy是形参,调用时如hypot(3, 4)34是实参 ,函数返回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) float64Sin函数使用汇编语言实现 。

递归

  • 函数可递归调用,即直接或间接调用自身 ,能处理具有递归特性的数据结构 。
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.Containsstrconv.FormatBool ,对所有可能参数都有定义好的返回 ,即使存在内存耗尽等极端情况,其错误表现和起因复杂且恢复渺茫 。
  • 部分函数符合前置条件就能成功返回,如time.Date ,但参数不当会导致宕机 。
  • 还有很多函数受外部因素影响,不能保证一定成功返回,如 I/O 操作函数 ,这些地方是错误处理的重点 。

返回方式

  • 函数调用发生错误时,习惯将错误值作为最后一个结果返回 。若错误情况单一,结果常设为布尔类型 ,如cache.Lookup ,成功返回值和true ,键不存在返回false 。对于 I/O 等操作,错误原因多样,返回类型常为error

error 类型

  • error是内置接口类型 ,一个错误可能为空值(表示成功 )或非空值(表示失败 ) ,非空错误类型有错误消息字符串 ,可通过Error方法或fmt.Printlnfmt.Printf输出错误消息 。

错误处理原则

  • 当函数返回非空错误时,其他结果通常无定义应忽略 ,但有些函数出错时会返回部分可用结果 ,调用者应先处理错误 。Go 语言通过普通返回值报告错误 ,而非异常机制(Go 语言异常仅针对程序 bug 导致的预料外错误 ) ,使用常规控制流(如ifreturn )处理错误 ,虽要求更谨慎,但这是设计要点 。

错误处理策略

  1. 传递错误

当函数调用返回错误时,将错误直接传递给调用者 。如findLi nks函数中,http.Get失败时直接返回错误 ;html.Parse失败时,构建包含相关信息的新错误消息再返回 。设计错误消息要慎重,包含充足相关信息且保持一致 ,像os包的文件操作函数返回的错误就包含文件名字等信息 。

  1. 重试操作

对于不固定或不可预测的错误,在短时间间隔后重试 。如WaitForServer函数尝试连接 URL 对应的服务器 ,设置超时时间,在规定时间内多次重试 ,超过重试次数和限定时间后报错退出 。

  1. 输出错误并停止程序

若错误无法顺利解决,输出错误信息后优雅停止程序 。一般库函数将错误传递给调用者,主程序部分可处理错误并停止 ,可使用log.Fatalf实现,也可自定义日志输出格式 。

  1. 记录错误并继续运行

在一些错误情况下,仅记录错误信息,程序继续运行 。可使用log包记录日志,也可直接输出到标准错误流 。

  1. 忽略错误

在某些特殊情况(如操作系统会周期性清理临时目录 )下,直接忽略错误 。但有意忽略错误时要清楚逻辑后果,函数错误处理通常在开头检查并返回错误,再执行实际函数体 。

文件结束标识

当从文件读取数据时,若要读取的字节数为文件长度,任何错误都代表操作失败 ;若反复读取固定大小数据块直到文件耗尽,就需区分读取到文件尾和遇到其他错误的情况 。

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.ReaderReadRune方法读取 ,当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 ,在函数体内valsint类型的 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)
}
  • 修改返回结果:延迟执行的匿名函数可更新函数结果变量 ,如doubletriple函数通过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程序设计语言》


网站公告

今日签到

点亮在社区的每一天
去签到