06_函数

发布时间 2023-10-08 13:14:39作者: Stitches

一、函数介绍

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("过滤完毕...")
}

参考:

如何在Go中逐个字符地读取文件

golang 字符处理/转换/操作(string/byte/rune)

用golang实现替换某个文件中的字符串

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

https://blog.51cto.com/u_536410/4750136

https://juejin.cn/post/7140664403996868615