一、函数介绍
Go 函数的特点:
- 无需声明原型
- 支持不定变参
- 支持多返回值
- 支持匿名函数和闭包
- 函数也是一种类型,一个函数可以赋值给变量
- 函数不支持重载,不支持默认参数
Go 语言里面含有三种类型的函数:
- 普通带有名字的函数
- 匿名函数或者 lambda 函数
- 方法(Methods)
注意Go 语言区别于其它语言:
- 不允许函数重载,函数重载是指可以编写多个同名函数,但拥有不同的形参/或者不同的返回值;
- 函数可以以申明的方式被使用,作为一个函数类型,比如
type binOp func(int, int) int
- 目前Go没有泛型的概念,也就是说不支持那种支持多种类型的函数,但是可以通过接口或者反射来实现类似的功能
1.1 函数按值传递和按引用传递
Go 默认使用按值传递的方式来传递参数,也就是传递参数的副本,这样在函数体内对副本进行修改不会影响到原来的变量值。
如果你希望函数可以修改参数的值就需要按引用传递,也就是传递给函数参数一个指针,我们可以通过这个指针的值来修改这个值所指向的地址上的值。几乎在任何情况下,传递指针 (一个32位或者64位的值) 的消耗都比传递副本来的少。
在函数调用时,像切片(slice)、字典(map)、接口(interface)、通道(channel) 这些引用类型都是默认使用引用传递。
1.2 命名返回值
当需要返回多个非命名返回值时,需要使用括号括起来,同时返回时不能省略 return xxx;
当需要返回多个命名返回值时,可以简单使用一个 return 语句。
func getX2AndX3(input int) (int, int) { // 非命名返回值
return 2 * input, 3 * input
}
func getX2AndX3_2(input int) (x2 int, x3 int) { // 命名返回值
x2 = 2 * input
x3 = 3 * input
// return x2, x3
return
}
1.3 传递变长参数
如果函数最后一个参数采用 ...type 的形式,这个函数就可以处理变长的参数。比如:
// 例1
func Greeting(prefix string, who ...string)
Greeting("hello:", "Joe", "Anna", "Eileen")
// 例2
func main() {
x := min(1, 3, 2, 0)
fmt.Printf("The minimum is: %d\n", x)
slice := []int{7,9,3,5,1}
x = min(slice...) //注意不要省略点
fmt.Printf("The minimum in the slice is: %d", x)
}
func min(s ...int) int {
if len(s)==0 {
return 0
}
min := s[0]
for _, v := range s {
if v < min {
min = v
}
}
return min
}
如果需要传递变长参数,但是类型不一致,那么可以使用 ...interface{} 来接受任何类型的参数,在使用时再通过 for-range 循环判断每个参数的类型。
func typecheck(..,..,values … interface{}) {
for _, value := range values {
switch v := value.(type) {
case int: …
case float: …
case string: …
case bool: …
default: …
}
}
}
1.4 内置函数
Go 语言内部包含一些不需要导包就可以直接使用的函数。
名称 | 说明 |
---|---|
close | 用于关闭管道 |
len、cap | len返回某个类型的长度或数量(字符串、数组、切片、map、管道);cap返回最大容量(数组、切片、管道,不能用于map) |
new、make | 都是分配内存,new用于值类型和用户自定义类型;make用于内置引用类型(切片、map、管道)。用法为new(type)、make(type)。 |
copy、append | 复制和连接切片 |
panic、recover | 错误处理,panic抛出异常、recover捕获异常 |
print、println | 打印 |
complex、real imag | 创建和操作复数 |
值类型和引用类型:值类型是指直接存储其值,而引用类型存储对其值的引用。
值类型包括:byte、short、int、long、float、double、decimal、char、bool、struct;引用类型包括 class、string
值类型变量在申明后,无论是否赋值,编译器已经为它分配内存;引用类型当声明一个类时,只在栈中分配一小片内存用于容纳一个地址,而此时并没有为其分配堆上的内存空间。当使用 new 创建一个类的实例时,分配堆上的空间,并把堆上空间的地址保存到栈上分配的小片空间中。
值类型通常在线程栈上分配(静态分配),某些情况下可以存储在堆中;引用类型在进程堆中分配。
二、注意点
2.1 将函数作为参数
函数可以作为其它函数的参数进行传递,然后在其它函数内调用执行。
// 定义函数类型
type FormatFunc func(s string, x, y int) string
func format(fn FormatFunc, s string, x, y int) string {
return fn(s, x, y)
}
func main() {
s1 := TestFn(func() int { //匿名函数当作参数
return 18
})
s2 := format(func(s string, x, y int) string {
return fmt.Sprintf(s, x, y)
}, "%d,%d", 10, 20)
println(s1, s2)
}
func main() {
callback(1, Add)
}
func Add(a, b int) {
fmt.Printf("The sum of %d and %d is: %d\n", a, b, a+b)
}
func callback(y int, f func(int, int)) {
f(y, 2) // this becomes Add(1, 2)
}
//输出
The sum of 1 and 2 is: 3
一个特别的例子是 strings.IndexFunc() 方法:func IndexFunc(s string, f func(c rune) bool) int
,它的返回值是字符串中第一个使 f(c) 为 true 的Unicode 字符的位置,如果找不到则为-1。例如 strings.IndexFunc(line, unicode.IsSpace)
就会返回 line 中第一个空白字符的索引。
练习:实现一个程序,要求将指定文本内的所有非 ASCII 字符替换为 ?
func IsUnicode(r rune) bool {
return unicode.Is(unicode.ASCII_Hex_Digit, r)
}
// 文件内非ASCII字符替换 --- bufio.NewReader方式
func FilterFile(path string) {
fmt.Println("开始过滤...")
// 打开、新建文件
infile, err := os.Open(path)
if err != nil {
panic(err)
}
defer infile.Close()
outfile, err := os.OpenFile("temp.txt", os.O_CREATE|os.O_RDWR, 0766)
if err != nil {
panic(err)
}
defer outfile.Close()
// 逐行读取,替换,写入
in := bufio.NewReader(infile)
out := bufio.NewWriter(outfile)
for {
r, _, err := in.ReadRune()
if err == io.EOF {
break
}
if err != nil {
panic(err)
os.Exit(-1)
}
if IsUnicode(r) {
out.WriteRune(r)
} else {
out.WriteString("?")
}
}
out.Flush()
fmt.Println("过滤完毕...")
}
// 文件内非ASCII字符替换 --- bufio.NewScanner方式
func FilterFile2(path string) {
fmt.Println("开始过滤...")
// 打开、新建文件
infile, err := os.Open(path)
if err != nil {
panic(err)
}
defer infile.Close()
outfile, err := os.OpenFile("temp.txt", os.O_CREATE|os.O_RDWR, 0766)
if err != nil {
panic(err)
}
defer outfile.Close()
// 逐行读取,替换,写入
in := bufio.NewScanner(infile)
in.Split(bufio.ScanRunes)
out := bufio.NewWriter(outfile)
for in.Scan() {
bts := in.Bytes()
r, _ := utf8.DecodeRune(bts)
if IsUnicode(r) {
out.WriteRune(r)
} else {
out.WriteString("?")
}
}
out.Flush()
fmt.Println("过滤完毕...")
}
参考:
golang 字符处理/转换/操作(string/byte/rune)
2.2 闭包
闭包是由函数及其相关引用环境组合而成的实体(即:闭包 = 函数 + 引用环境),下面看一下 JavaScript 的闭包:
<!DOCTYPE html>
<html lang="zh">
<head>
<title></title>
</head>
<body>
</body>
</html>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.2.6/jquery.min.js" type="text/javascript"></script>
<script>
function a(){
var i=0;
function b(){
console.log(++i);
document.write("<h1>"+i+"</h1>");
}
return b;
}
$(function(){
var c=a();
c();
c();
c();
//a(); //不会有信息输出
document.write("<h1>=============</h1>");
var c2=a();
c2();
c2();
});
</script>
函数b() 嵌套在函数a() 内部,函数a() 返回函数b(),这样在执行完 var c = a()后,多次调用 c()会导致内部变量 i 递增。在给定函数的多次调用过程中,这些私有变量能够保持其持久性,虽然变量的作用域仅限于包含他们的函数,但是变量的生存周期可以很长,在一次函数调用期间所产生的值在下次函数调用时仍然存在,因此闭包可以用来做信息隐藏。
但是如果a()返回的不是函数b(),情况就完全不同了。因为a()执行完后,b()没有被返回给a()的外界,只是被a()所引用,而此时a()也只会被b()引 用,因此函数a()和b()互相引用但又不被外界打扰(被外界引用),函数a和b就会被GC回收。所以直接调用a();是页面并没有信息输出。
再说闭包的另一种引用环境,c()跟c2()引用的是不同的环境,在调用i++时修改的不是同一个i,因此两次的输出都是1。函数a()每进入一次,就形成了一个新的环境,对应的闭包中,函数都是同一个函数,环境却是引用不同的环境。这和c()和c()的调用顺序都是无关的。
当我们不希望给函数起名字的时候可以使用匿名函数,我们可以直接使用匿名函数,也可以将匿名函数赋值给变量并对其进行调用。
func() {
sum := 0
for i := 1; i <= 1e6; i++ {
sum += i
}
}()
defer 与 匿名函数搭配可以用于改变函数的命名返回值,也可以用于在函数执行过程中释放锁。
func f() (ret int) {
defer func() {
ret++
}()
return 1
}
func main() {
fmt.Println(f())
}
// 输出的 ret 值为2,因为defer中的 ret++ 是在 return 1显式返回前执行的
2.3 闭包的应用(将函数作为返回值并被外部引用)
// 例1
func main() {
p2 := Add2()
fmt.Printf("Call Add2 for 3 gives: %v\n", p2(3))
TwoAdder := Adder(2)
fmt.Printf("The result is: %v\n", TwoAdder(3))
}
func Add2() func(b int) int {
return func(b int) int {
return b + 2
}
}
func Adder(a int) func(b int) int {
return func(b int) int {
return a + b
}
}
// 输出
Call Add2 for 3 gives: 5
The result is: 5
// 例2
func main() {
var f = Adder()
fmt.Print(f(1), " - ")
fmt.Print(f(20), " - ")
fmt.Print(f(300))
}
func Adder() func(int) int {
var x int
return func(delta int) int {
x += delta
return x
}
}
//输出
1 - 21 - 321
可以发现,在多次调用时 x 的值都得到了保存,闭包函数会保存并积累其中变量的值,不管外部函数退出与否,它都能够继续操作外部函数中的局部变量,也就是说闭包函数会持续保存局部变量的值,这里的局部变量包括 ①外部函数的局部变量;②外部函数的参数;
可以通过定义的闭包返回的函数,在该函数内使用 runtime
或者 log
中的特殊函数来显示代码执行的行数。
func TestNonamePack() {
where := func() {
_, file, line, _ := runtime.Caller(1)
log.Printf("%s:%d", file, line)
}
var a = 0
var b = 1
where()
c := a + b
fmt.Printf("%d", c)
where()
}
使用时直接调用 where()
函数就可以打印代码执行位置。
三、defer与闭包
defer 具有如下特性:
- defer 用于注册延迟调用,将defer后边的函数压入栈中,当前函数返回前再把延迟函数取出并执行;
- 调用直到 return 前才执行,可以用来做资源清理;
- 多个defer 按照先进后出的方式执行;
- defer 定义时对外部变量的引用有两种方式,分别是作为函数参数、作为闭包引用,作为函数参数则在 defer 定义时就传递给defer后的函数缓存起来;作为闭包引用则会在 defer后函数真正执行时根据上下文确定当前的值。
defer的用途:
- 关闭文件句柄
- 锁资源释放
- 数据库连接释放
3.1 测试实例
func increaseA() int {
var i int
defer func() {
i++
}()
return i
} //out: 0
func increaseB() (r int) {
defer func() {
r++
}()
return r //out: 1
}
func increaseC() (r int) {
defer func(r int) {
r++
}(r)
return r //out: 0
}
原因分析:
函数内 return 不是一个原子操作,它会拆分为 (1)返回变量 = xxx;(2)调用defer函数;(3)return 返回变量;
这里的返回变量需要根据函数签名区分,如果函数签名中定义了返回变量,那么就是它;如果没有定义,那么返回变量就是一个匿名变量。defer 函数能够更改函数返回值的情况,都是在函数签名中定义了返回变量的情景。
//这里举例讲解
1.
func f1() (r int) {
defer func() {
r++
}()
return 0 //实际return 1
}
/* 过程拆分为:
r = 0 // 1. 赋值
func() { // 2. 运行 defer 函数 r++,r = 1
r++
}()
return r // 3. return,即返回结果为 1
*/
2.
func f2() (r int) {
t := 5
defer func() {
t = t + 5
}()
return t //实际 return 5
}
/* 过程拆分为:
r = t (= 5) // 1. 赋值,r 取值 5
func() { // 2. 执行 defer 函数,执行后 t = 10,但 r = 5
t = t + 5
}()
return r // 3. return r,即返回 5
*/
3.
func f3() (r int) {
defer func(r int) { // 作为函数参数传入 defer 函数
r = r + 5
}(r)
return 1 //实际 return 1
}
/* 过程拆分为:
r = 1 // 1. 赋值, r 取值 1
func(r int) { // 2. 执行 defer 函数,但作为函数参数传入
r = r + 5 // 执行后 r = 6 ,但这是局部变量,函数外仍是 1
}(r)
return r // 3. return r, 即返回 1
*/
3.2 总结
- defer 函数的参数,在 defer 定义时就把值传递给 defer,并被缓存起来;
- 如果作为闭包引用,那么变量的值需要根据 defer函数执行时的具体上下文来确定。
- defer 中传入的参数都是作为局部变量了,除非匿名函数或者引用传入。
四、闭包使用场景
4.1 隔离数据
如果希望创建一个函数,使得当函数执行结束时仍然能够访问到函数的内部变量;或者不希望其它人访问该数据,可以使用闭包来实现:
func makeFibGen() func() int {
f1 := 0
f2 := 1
return func() int {
f2, f1 = (f1 + f2), f2
return f1
}
}
func main() {
gen := makeFibGen()
for i := 0; i < 10; i++ {
fmt.Println(gen())
}
}
4.2 封装函数和创建中间件
go语言中可以创建匿名函数,同时函数也是一个特殊的参数。比如在创建 web 服务器时,通常会提供一个函数来处理特定路由的 Web 请求,如下:
func main() {
http.HandleFunc("/hello", hello)
http.ListenAndServe(":3000", nil)
}
func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "<h1>Hello!</h1>")
}
如果我们希望在 hello 函数执行前后执行一些额外的逻辑,就可以通过闭包返回一个可以作为 handleFunc 参数的函数。
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
http.HandleFunc("/hello", timed(hello))
http.ListenAndServe(":3000", nil)
}
func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "<h1>Hello!</h1>")
}
//通过闭包创建特定函数,能够返回访问的时差
func timed(f func(w http.ResponseWriter, r *http.Request)) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
f(w, r)
end := time.Now()
fmt.Println("The request took", end.Sub(start))
}
}
4.3 访问通常不可用的数据
关于 http.HandleFunc
函数,在直接调用时不能传递参数:
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
如果我们希望使用时传递自定义变量,可以通过闭包创建新的函数类型进行参数传递:
package main
import (
"fmt"
"net/http"
)
type Database struct {
Url string
}
func NewDatabase(url string) Database {
return Database{url}
}
func main() {
db := NewDatabase("localhost:5432")
http.HandleFunc("/hello", hello(db))
http.ListenAndServe(":3000", nil)
}
func hello(db Database) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, db.Url)
}
}
参考:
https://www.jianshu.com/p/b4fb3d361d87