【Go语言圣经】第五节:函数

第五章:函数

5.1 函数声明

和其它语言类似,Golang 的函数声明包括函数名、形参列表、返回值列表(可省略)以及函数体:

func name(parameter-list) (result-list) {/* ... Body ... */
}

需要注意的是,函数的返回值列表可省略,如果一个函数声明不包括返回值列表,那么函数体执行完毕后,不会返回任何值。

一个函数声明的例子如下:

func hypot(x, y float64) float64 {return math.Sqrt(x * x + y * y)
}

如果一组形参或返回值具有相同的类型,我们就不必为每个形参都写出参数类型:

func f(i, j, k int, s, t string)	{ /*... ... ...*/ }
// 等价于
func f(i int, j int, k int, s string, t string	{ /*... ... ...*/ }

函数的类型被称为函数的签名。如果两个函数的形参列表和返回值列表中的变量类型一一对应,那么这两个函数被认为具有相同的类型或签名。形参和返回值的变量名不影响函数签名,也不影响它们是否可以以省略参数类型的形式表示。

每次函数调用必须按照声明顺序为所有参数提供实参。在函数调用时,Golang 没有默认参数值,也没有办法可以通过参数名指定形参,因此形参和返回值的变量名对函数调用无意义。

实参通过值的方式传递,因此函数的形参是实参的拷贝。对形参的修改不会影响实参。但如果实参包括引用类型,比如指针、slice、map、func、channel 等,实参可能由于函数的间接引用被修改。

5.2 递归

大部分语言使用固定大小的函数调用栈,固定大小的栈会限制递归的深度,当用递归处理大量数据时,需要避免栈溢出;此外,还会导致安全性问题。与此相反,Golang 使用可变栈,栈的大小按需增加(初始时很小),这使得递归不必考虑溢出和安全性问题。

5.3 多返回值

在 Golang 当中,一个函数可以返回多个值。常见的有许多标准库的函数返回两个值,一个值是期望得到的值,另一个是函数出错的错误信息。

调用多返回值函数时,返回给调用者的是一组值,调用者必须显式地将这些值分配给变量:

links, err := findLinks(url)
// OR
links, _ := findLinks(url)

如果一个函数所有的返回值都有显式的变量名,那么该函数的 return 语句可以省略操作数,称为 bare return:

// CountWordsAndImages does an HTTP GET request for the HTML
// document url and returns the number of words and images in it.
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	// 返回 words, images, err
}
func countWordsAndImages(n *html.Node) (words, images int) { /* ... */ }

bare return 使得代码难以理解,因此不宜过度使用。

5.4 错误

在 Golang 当中,函数调用发生错误时,错误的信息通常通过 error 类型以函数返回值的形式反馈给函数调用者。

内置的 error 是接口类型,但由于我们还没复习到接口,目前只需要知道 error 类型可能是 nil 或者 non-nil。nil 意味着函数运行成功,non-nil 表示失败。

可以调用 error 的 Error 函数或输出函数获得字符串类型的错误信息。

通常在函数返回 non-nil 的 error 时,其它的返回值可能是未定义的(undefined),这些未定义的返回值应该忽略。

Go 使用控制流机制(如 if 和 return)处理错误,这使得编码人员能更多地关注错误处理。

5.4.1 错误处理策略

最常用的方式是传播错误:

resp, err := http.Get(url)
if err != nil {return nil, 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)
}

第二种错误处理的策略是,如果错误是偶然发生的,或由不可预知的问题导致的,一个明智的选择是重新尝试失败的操作:

func WaitForServer(url string) error {const timeout = 1 * time.Minutedeadline := 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 << unit(tries))	// exponential back-off}return fmt.Errorf("server %s failed to respond after %s", url, timeout)
}

如果错误发生,程序无法继续运行,我们可以采取第三种策略:输出错误信息并结束程序。需要注意的是,这种策略只能在 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", nil)
}

最后一种策略:直接忽略错误。

5.4.2 文件结尾错误(EOF)

Golang 的 io 包保证任何由文件结束引起的读取失败都会返回同一个错误——io.EOF

package io
import "errors"
var EOF = errors.New("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)}
}

5.5 函数值

在 Golang 当中,函数被视为一等公民(first-class values):函数像其他值一样,拥有类型,可以被赋值给其它变量,传递给函数,从函数返回。对函数值的调用类似函数调用:

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			// 指定了函数的类型为 func(int)
fmt.Println(f(3))f - negative
fmt.Println(f(3))f = product			// 错误❌: 不能将 func(int, int) 赋值给 func(int) 类型

函数类型的零值为 nil,调用值为 nil 的函数值会引起 panic 错误。函数值可以和 nil 进行比较。

函数值使得我们不仅可以通过数据来参数化函数,亦可以通过行为。下例展示了使用 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"

5.6 匿名函数

拥有函数名的函数只能在包级语法块中被使用,通过函数字面量(function literal),我们可以绕过这一限制,在任何表达式中表示一个函数值。

函数字面量的语法和函数声明相似,区别在于 func 关键字后面没有函数名。函数值字面量是一种表达式,它的值被称为匿名函数(anonymous function)。

下例改写了之前例子中使用 strings.Map 调用 Add1 的例子:

strings.Map(func(r rune) rune { return r + 1 }, "HAL-9000")

通过上述方式定义的函数可以访问完整的词法环境(lexical environment),这意味着在函数中定义的内部函数可以引用该函数的变量:

// 也被称为函数闭包
func squares() func() int {	// squares 的返回值是匿名函数, 该匿名函数的返回值是 intvar x int				// 在闭包中定义 xreturn 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 的例子证明,Golang 当中的函数不仅仅是一串代码,它们还记录了函数内部的状态。通过这个例子,我们也看到变量的生命周期不由它的作用域决定,返回 squares 后,变量 x 仍然隐式存在于 f 中。

5.6.1 警告:捕获迭代变量

本节将介绍 Golang 词法作用域的一个陷阱。

考虑下述问题:你被要求首先创建一些目录,之后删除。正确的示例如下:

var rmdirs []func()
for _, d := range tempDirs() {dir := d	// 注意: 这一步是必须的os.MkdirAll(dir, 0755)rmdirs = append(rmdirs, func() {os.RemoveAll(dir)})
}for _, rmdir := rmdirs {rmdir()
}

我们可能感到困惑,为什么要在循环体内用循环变量 d 赋值给一个新的局部变量 dir,而不是直接使用循环变量 d?问题的原因在于循环变量的作用域。在上面的程序中,for 循环引入了新的词法块,循环变量 dir 在这个词法中被声明。在该循环中的所有函数值都共享相同的循环变量。以 d 为例,后续的迭代会不断更新 d 的值,当删除操作执行时,for 循环已经完成,d 当中存储的值等于最后一次迭代的值,这意味着,每次对 os.RemoveAll 调用的结果都是删除相同的目录。

通常为了解决上述问题,都会引入一个与循环变量同名或相似的局部变量,作为循环变量的副本。

上述问题不仅存在于基于 range 的循环当中,使用循环变量 i 时也存在相同问题。

5.7 可变参数

参数数量可变的参数称为可变参数函数,典型的例子是fmt.Printf及类似函数。

在声明可变参数函数时,需要在参数列表的最后一个参数类型之前添加省略号"..."

func sum(vals ...int) int {total := 0for _, val := range vals {total += val]return total
}

上述 sum 函数返回任意个 int 型参数的和。在函数体中,vals 被看作类型为[]int的切片。

如果原始参数已经是切片类型,如何传递给 sum ?只需要在最后一个参数后加上省略号:

values := []int{1, 2, 3, 4}
fmt.Println(sum(values...))

实际上,可变参数函数和以切片作为参数的函数是不同的:

func f(...int) {}
func g([]int)  {}
fmt.Printf("%T\n", f)	// func(...int)
fmt.Printf("%T\n", g)	// func([]int)

可变参数函数常被用于格式化字符串。下面的 errorf 函数构造了一个以行号开头的,经过格式化的错误信息。函数名的后缀 f 是一种通用的命名规范,代表该可变参数函数可以接收 Printf 风格的格式化字符串:

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"

其中interface{}表示函数的最后一个参数可以接收任意类型。

5.8 Deferred 函数

只需要在普通调用函数或方法前加上 defer 关键字,就完成了 defer 所需要的语法。当执行到该语句时,函数和参数表达式得到计算,但直到包含该 defer 语句的函数执行完毕时,defer 后的语句才会执行。

可以在一个函数中执行多条 defer,它们的执行顺序与声明顺序相反

5.9 Panic 异常

一般而言,panic 发生时,程序会中断,并立即执行在该 goroutine 中被延迟(defer)的函数。

不是所有 panic 都来自运行时,直接调用内置的 panic 函数也会引发 panic 异常。

虽然 Go 的 panic 机制类似于其它语言的异常,但使用场景略微不同。由于 panic 会引起程序崩溃,因此 panic 一般只用于严重错误。对于大部分漏洞,我们应该使用 Go 提供的错误机制,而不是 panic,尽量避免程序崩溃。

5.10 Recover 捕获异常

通常来说,不应该对 panic 异常做任何处理,但有时我们希望程序可以从异常中恢复。

如果在deferred函数中调用了内置函数recover,并且定义该defer语句的函数发生了panic异常,recover会使程序从panic中恢复,并返回panic value。导致panic异常的函数不会继续运行,但能正常返回。在未发生panic时调用recover,recover会返回nil。

以 Parse 为例,说明 recover 的使用场景:

func Parse(input string) (s *Syntax, err error) {defer func() {if p := recover(); p != nil {err = fmt.Errorf("internal error: %v", p)}}// ... parser ...
}

recover 帮助 Parse 从 panic 恢复。在 deferred 函数内部,panic value 被附加到错误信息中。

我们不应该试图去恢复其他包引起的 panic,也不应该恢复由他人开发的函数引起的 panic。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/9647.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

(2)SpringBoot自动装配原理简介

SpringBoot自动装配 这里写目录标题 SpringBoot自动装配启动器主程序自定义扫描包SpringBootApplicationSpringBootConfigurationEnableAutoConfigurationAutoConfigurationPackageImport({AutoConfigurationImportSelector.class})选择器AutoConfigurationEntrygetCandidateCo…

计算机网络 (60)蜂窝移动通信网

一、定义与原理 蜂窝移动通信网是指将一个服务区分为若干蜂窝状相邻小区并采用频率空间复用技术的移动通信网。其原理在于&#xff0c;将移动通信服务区划分成许多以正六边形为基本几何图形的覆盖区域&#xff0c;称为蜂窝小区。每个小区设置一个基站&#xff0c;负责本小区内移…

17.Word:李楠-学术期刊❗【29】

目录 题目​ NO1.2.3.4.5 NO6.7.8 NO9.10.11 NO12.13.14.15 NO16 题目 NO1.2.3.4.5 另存为手动/F12Fn光标来到开头位置处→插入→封面→选择花丝→根据样例图片&#xff0c;对应位置填入对应文字 (手动调整即可&#xff09;复制样式&#xff1a;开始→样式对话框→管理…

Java面试题2025-并发编程基础(多线程、锁、阻塞队列)

并发编程 一、线程的基础概念 一、基础概念 1.1 进程与线程A 什么是进程&#xff1f; 进程是指运行中的程序。 比如我们使用钉钉&#xff0c;浏览器&#xff0c;需要启动这个程序&#xff0c;操作系统会给这个程序分配一定的资源&#xff08;占用内存资源&#xff09;。 …

Java创建项目准备工作

新建项目 新建空项目 每一个空项目创建好后都要检查jdk版本 检查SDK和语言级别——Apply——OK 检查当前项目的Maven路径&#xff0c;如果已经配置好全局&#xff0c;就是正确路径不用管 修改项目字符集编码&#xff0c;将所有编码都调整为UTF-8 创建Spingboot工程 创建Spring…

2007-2020年各省国内专利申请授权量数据

2007-2020年各省国内专利申请授权量数据 1、时间&#xff1a;2007-2020年 2、来源&#xff1a;国家统计局、统计年鉴 3、指标&#xff1a;行政区划代码、地区名称、年份、国内专利申请授权量(项) 4、范围&#xff1a;31省 5、指标解释&#xff1a;专利是专利权的简称&…

(一)QT的简介与环境配置WIN11

目录 一、QT的概述 二、QT的下载 三、简单编程 常用快捷键 一、QT的概述 简介 Qt&#xff08;发音&#xff1a;[kjuːt]&#xff0c;类似“cute”&#xff09;是一个跨平台的开发库&#xff0c;主要用于开发图形用户界面&#xff08;GUI&#xff09;应用程序&#xff0c;…

【C语言】main函数解析

一、前言 在学习编程的过程中&#xff0c;我们很早就接触到了main函数。在Linux系统中&#xff0c;当你运行一个可执行文件&#xff08;例如 ./a.out&#xff09;时&#xff0c;如果需要传入参数&#xff0c;就需要了解main函数的用法。本文将详细解析main函数的参数&#xff…

自创《艺术人生》浅析

艺术是生活的馈赠&#xff0c;艺术是苦痛的呻吟。 笔记模板由python脚本于2025-01-29 00:01:11创建&#xff0c;本篇笔记适合喜欢写诗读诗诵诗的coder翻阅。 【学习的细节是欢悦的历程】 博客的核心价值&#xff1a;在于输出思考与经验&#xff0c;而不仅仅是知识的简单复述。 …

二进制安卓清单 binary AndroidManifest - XCTF apk 逆向-2

XCTF 的 apk 逆向-2 题目 wp&#xff0c;这是一道反编译对抗题。 题目背景 AndroidManifest.xml 在开发时是文本 xml&#xff0c;在编译时会被 aapt 编译打包成为 binary xml。具体的格式可以参考稀土掘金 MindMac 做的类图&#xff08;2014&#xff09;&#xff0c;下面的博…

AboutDialog组件的功能和用法

文章目录 1 概念介绍2 使用方法3 示例代码 我们在上一章回中介绍了AlertDialog Widget相关的内容,本章回中将介绍AboutDialog Widget.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1 概念介绍 我们在这里说的AboutDialog是一种弹出式窗口&#xff0c;和上一章回中介绍的Al…

计算机网络 IP 网络层 2 (重置版)

IP的简介&#xff1a; IP 地址是互联网协议地址&#xff08;Internet Protocol Address&#xff09;的简称&#xff0c;是分配给连接到互联网的设备的唯一标识符&#xff0c;用于在网络中定位和通信。 IP编制的历史阶段&#xff1a; 1&#xff0c;分类的IP地址&#xff1a; …

关于产品和技术架构的思索

技术架构或者设计应该和产品设计分离&#xff0c;但是又不应该和产品架构独立。 听起来非常的绕并且难以理解。 下面我们用一个例子来解读这两者的关系 产品&#xff08;族谱图&#xff09; 如果把人类当作产品&#xff0c;那设计师应该是按照上面设计的(当然是正常的伦理道德)…

第十四讲 JDBC数据库

1. 什么是JDBC JDBC&#xff08;Java Database Connectivity&#xff0c;Java数据库连接&#xff09;&#xff0c;它是一套用于执行SQL语句的Java API。应用程序可通过这套API连接到关系型数据库&#xff0c;并使用SQL语句来完成对数据库中数据的查询、新增、更新和删除等操作…

蓝牙技术在物联网中的应用有哪些

蓝牙技术凭借低功耗、低成本和易于部署的特性&#xff0c;在物联网领域广泛应用&#xff0c;推动了智能家居、工业、医疗、农业等多领域发展。 智能家居&#xff1a;在智能家居系统里&#xff0c;蓝牙技术连接各类设备&#xff0c;像智能门锁、智能灯泡、智能插座、智能窗帘等。…

Visual Studio Code修改terminal字体

个人博客地址&#xff1a;Visual Studio Code修改terminal字体 | 一张假钞的真实世界 默认打开中断后字体显示如下&#xff1a; 打开设置&#xff0c;搜索配置项terminal.integrated.fontFamily&#xff0c;修改配置为monospace。修改后效果如下&#xff1a;

开发环境搭建-4:WSL 配置 docker 运行环境

在 WSL 环境中构建&#xff1a;WSL2 (2.3.26.0) Oracle Linux 8.7 官方镜像 基本概念说明 容器技术 利用 Linux 系统的 文件系统&#xff08;UnionFS&#xff09;、命名空间&#xff08;namespace&#xff09;、权限管理&#xff08;cgroup&#xff09;&#xff0c;虚拟出一…

71-《颠茄》

颠茄 颠茄&#xff0c;别名&#xff1a;野山茄、美女草、别拉多娜草&#xff0c;拉丁文名&#xff1a;Atropa belladonna L.是双子叶植物纲、茄科、颠茄属多年生草本&#xff0c;或因栽培为一年生&#xff0c;根粗壮&#xff0c;圆柱形。茎下部单一&#xff0c;带紫色&#xff…

2025 = 1^3 + 2^3 + 3^3 + 4^3 + 5^3 + 6^3 + 7^3 + 8^3 + 9^3

【算法代码】 #include <bits/stdc.h> using namespace std;int year2025; int main() {cout<<year<<" ";int i1;while(year) {cout<<i<<"^3";if(i<9) cout<<" ";year-pow(i,3);i;}return 0; }/* 202…

三天急速通关JavaWeb基础知识:Day 1 后端基础知识

三天急速通关JavaWeb基础知识&#xff1a;Day 1 后端基础知识 0 文章说明1 Http1.1 介绍1.2 通信过程1.3 报文 Message1.3.1 请求报文 Request Message1.3.2 响应报文 Response Message 2 XML2.1 介绍2.2 利用Java解析XML 3 Tomcat3.1 介绍3.2 Tomcat的安装与配置3.3 Tomcat的项…