[Golang]多返回值函数、defer关键字、内置函数、变参函数、类成员函数、匿名函数-CSDN博客

参考书《the way to go》

Go 里面有三种类型的函数

  • 普通的带有名字的函数
  • 匿名函数或者lambda函数
  • 方法

没有参数的函数通常被称为 niladic 函数 (niladic function)

函数可以将其他函数调用作为它的参数只要这个被调用函数的返回值个数、返回值类型和返回值的顺序与调用函数所需求的实参是一致的

假设 f1 需要 3 个参数 f1(a, b, c int)同时 f2 返回 3 个参数 f2(a, b int) (int, int, int)就可以这样调用 f1f1(f2(a, b))

Go不允许函数重载因为多余的类型匹配会影响性能

就是以函数作为返回类型。这种类型的声明要写在函数名和可选的参数列表之后例如

func FunctionName (a typea, b typeb) typeFunc

可以定义没有形参名的函数

func f(int, int, float64)

你可以在函数体中的某处返回使用类型为 typeFunc 的变量 var

return var

可拥有多种返回值

返回类型之间需要使用逗号分割并使用小括号 () 将它们括起来如

func FunctionName (a typea, b typeb) (t1 type1, t2 type2)

返回的形式

return var1, var2

这种多返回值一般用于判断某个函数是否执行成功 (true/false) 或与其它返回值一同返回错误消息

函数也可以以申明的方式被使用作为一个函数类型就像

type binOp func(int, int) int

在这里不需要函数体 {}

函数是一等值 (first-class value)它们可以赋值给变量就像 add := binOp 一样。

这个变量知道自己指向的函数的签名所以给它赋一个具有不同签名的函数值是不可能的。

函数值 (functions value) 之间可以相互比较如果它们引用的是相同的函数或者都是 nil 的话则认为它们是相同的函数。函数不能在其它函数里面声明不能嵌套不过我们可以通过使用匿名函数参考来破除这个限制。

目前 Go 没有泛型 (generic) 的概念也就是说它不支持那种支持多种类型的函数。不过在大部分情况下可以通过接口 (interface)特别是空接口与类型选择type switch与/或者通过使用反射reflection来实现相似的功能。使用这些技术将导致代码更为复杂、性能更为低下所以在非常注意性能的的场合最好是为每一个类型单独创建一个函数而且代码可读性更强

多返回值函数

多值返回是 Go 的一大特性为我们判断一个函数是否正常执行提供了方便。

我们通过 return 关键字返回一组值。事实上任何一个有返回值单个或多个的函数都必须以 returnpanic结尾

如果一个函数需要返回四到五个值我们可以传递一个切片给函数如果返回值具有相同类型或者是传递一个结构体如果返回值具有不同的类型。因为传递一个指针允许直接修改变量的值消耗也更少

实例

如下里的函数带有一个 int 参数返回两个 int 值其中一个函数的返回值在函数调用时就已经被赋予了一个初始零值。

getX2AndX3getX2AndX3_2 两个函数演示了如何使用非命名返回值与命名返回值的特性。当需要返回多个非命名返回值时需要使用 () 把它们括起来比如 (int, int)

命名返回值作为结果形参 (result parameters) 被初始化为相应类型的零值当需要返回的时候我们只需要一条简单的不带参数的 return 语句。需要注意的是即使只有一个命名返回值也需要使用 () 括起来

package main

import "fmt"

var num int = 10
var numx2, numx3 int

func main() {
    numx2, numx3 = getX2AndX3(num)
    PrintValues()
    numx2, numx3 = getX2AndX3_2(num)
    PrintValues()
}

func PrintValues() {
    fmt.Printf("num = %d, 2x num = %d, 3x num = %d\n", num, numx2, numx3)
}

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
}

输出结果

image-20231022193021981

按值传递、按引用传递

Go 默认使用按值传递来传递参数也就是传递参数的副本。函数接收参数副本之后在使用变量的过程中可能对副本的值进行更改但不会影响到原来的变量比如 Function(arg1)

几乎在任何情况下传递指针一个32位或者64位的值的消耗都比传递副本来得少。

在函数调用时像切片 (slice)、字典 (map)、接口 (interface)、通道 (channel) 这样的引用类型都是默认使用引用传递即使没有显式的指出指针。

有些函数只是完成一个任务并没有返回值。我们仅仅是利用了这种函数的副作用 (side-effect)就像输出文本到终端发送一个邮件或者是记录一个错误等。

但是绝大部分的函数还是带有返回值的。

类成员函数

package main

import "fmt"

type user struct {
	name     string
	password string
}

func (u *user) check2(password string) bool {
	return u.password == password
}

func (u *user) reset(password string) { //从一个普通函数变成类成员函数
	u.password = password
}

func main() {
	a := user{name: "wang", password: "1024"}
	a.reset("2048")
	fmt.Println(a.check2("2048"))
}

输出

true

改变外部变量

传递指针给函数不但可以节省内存因为没有复制变量的值而且赋予了函数直接修改外部变量的能力所以被修改的变量不再需要使用 return 返回。

如下的例子reply 是一个指向 int 变量的指针通过这个指针我们在函数内修改了这个 int 变量的数值

package main

import (
    "fmt"
)

// this function changes reply:
func Multiply(a, b int, reply *int) {
    *reply = a * b
}

func main() {
    n := 0
    reply := &n
    Multiply(10, 5, reply)
    fmt.Println("Multiply:", *reply) // Multiply: 50
}

当需要在函数内改变一个占用内存比较大的变量时性能优势就更加明显了。然而如果不小心使用的话传递一个指针很容易引发一些不确定的事所以我们要十分小心那些可以改变外部变量的函数在必要时需要添加注释以便其他人能够更加清楚的知道函数里面到底发生了什么。

变参函数

定义如果一个函数最后的参数传递的是...type 类型则可以处理变长的参数长度可以为0

这样的函数接受一个类似于slice的参数再使用for loop

//函数声明
func myFunc(a, b, arg ...int) {}

如果参数为 slice类型则可以通过 slice...来传递参数

package main

import "fmt"

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
}

输出

The minimum is: 0
The minimum in the slice is: 1

一个接受变长参数的函数可以将这个参数作为其它函数的参数进行传递

func F1(s ...string) {
	F2(s...)
	F3(s)
}

func F2(s ...string) { }
func F3(s []string) { }

变长参数可以作为对应类型的 slice 进行二次传递。

但是如果变长参数的类型并不是都相同的呢使用 5 个参数来进行传递并不是很明智的选择有 2 种方案可以解决这个问题

  1. 使用结构

    定义一个结构类型假设它叫 Options用以存储所有可能的参数

    type Options struct {
    	par1 type1,
    	par2 type2,
    	...
    }
    

    函数 F1() 可以使用正常的参数 ab以及一个没有任何初始化的 Options 结构 F1(a, b, Options {})。如果需要对选项进行初始化则可以使用 F1(a, b, Options {par1:val1, par2:val2})

  2. 使用空接口

    如果一个变长参数的类型没有被指定则可以使用默认的空接口 interface{}这样就可以接受任何类型的参数。该方案不仅可以用于长度未知的参数还可以用于任何不确定类型的参数。一般而言我们会使用一个 for-range 循环以及 switch 结构对每个参数的类型进行判断

    func typecheck(..,..,values … interface{}) {
    	for _, value := range values {
    		switch v := value.(type) {
    			case int:case float:case string:case bool:default:}
    	}
    }
    

defer和追踪

说明

关键字defer允许推迟到函数的任意位置执行return语句之后的一刻才执行某个语句或函数一般用于释放某些已分配的资源

用法类似于面向对象编程语言 Java 和 C# 的 finally 语句块它一般用于释放某些已分配的资源

package main
import "fmt"

func main() {
	function1()
}

func function1() {
	fmt.Printf("In function1 at the top\n")
	defer function2() //被延后
	fmt.Printf("In function1 at the bottom!\n")
}

func function2() {
	fmt.Printf("Function2: Deferred until the end of the calling function!")
}

输出

image-20231022200048723

使用 defer 的语句同样可以接受参数

//只是推迟输出并未推迟执行
func a() {
	i := 0
	defer fmt.Println(i)
	i++
	fmt.Println(i)
	return
}
//最后输出为0

当有多个 defer 行为被注册时它们会以逆序执行类似栈即后进先出

//最后输出为 4 3 2 1 0
func f() {
	for i := 0; i < 5; i++ {
		defer fmt.Printf("%d ", i)
	}
}

一些常见操作

//关闭文件流
defer file.Close()
//解锁一个加锁资源
muu.Lock()
defer mu.Unlock()
//打印最终报告
printHeader()
defer printFooter()
//关闭数据库连接
defer disconnectFromDB()

实现
package main

import "fmt"

func main() {
	doDBOperations()
}

func connectToDB() {
	fmt.Println("ok, connected to db")
}

func disconnectFromDB() {
	fmt.Println("ok, disconnected from db")
}

func doDBOperations() {
	connectToDB()
	fmt.Println("Defering the database disconnect.")
	defer disconnectFromDB() //function called here with defer
	fmt.Println("Doing some DB operations ...")
	fmt.Println("Oops! some crash or network error ...")
	fmt.Println("Returning from function here!")
	return //terminate the program
	// deferred function executed here just before actually returning, even if
	// there is a return or abnormal termination before
}

输出

image-20231022200619995

使用defer实现代码追踪

一个基础但十分实用的实现代码执行追踪的方案就是在进入和离开某个函数打印相关的消息即可以提炼为下面两个函数

func trace(s string) { fmt.Println("entering:", s) }
func untrace(s string) { fmt.Println("leaving:", s) }

以下代码展示了何时调用这两个函数

package main

import "fmt"

func trace(s string) string {
	fmt.Println("entering:", s)
	return s
}

func un(s string) {
	fmt.Println("leaving:", s)
}

func a() {
	defer un(trace("a"))
	fmt.Println("in a")
}

func b() {
	defer un(trace("b"))
	fmt.Println("in b")
	a()
}

func main() {
	b()
}

输出

image-20231022200850456

记录函数的参数和返回值

package main

import (
	"io"
	"log"
)

func func1(s string) (n int, err error) {
	defer func() {
		log.Printf("func1(%q) = %d, %v", s, n, err)
	}()
	return 7, io.EOF
}

func main() {
	func1("Go")
}

输出

Output: 2011/10/04 10:46:11 func1("Go") = 7, EOF

常见的内置函数

Go 语言拥有一些不需要进行导入操作就可以使用的内置函数。它们有时可以针对不同的类型进行操作例如len()cap()append()或必须用于系统级的操作例如panic()。因此它们需要直接获得编译器的支持。

以下是一个简单的列表

名称说明
close()用于管道通信
len()cap()len() 用于返回某个类型的长度或数量字符串、数组、切片、map 和管道cap() 是容量的意思用于返回某个类型的最大容量只能用于数组、切片和管道不能用于 map
new()make()new()make() 均是用于分配内存new() 用于值类型和用户定义的类型如自定义结构make 用于内置引用类型切片、map 和管道。它们的用法就像是函数但是将类型作为参数new(type)make(type)new(T) 分配类型 T 的零值并返回其地址也就是指向类型 T 的指针详见[第 10.1 节](file:///D:/self/资料/go/the-way-to-go/eBook/10.1.md)。它也可以被用于基本类型v := new(int)make(T) 返回类型 T 的初始化之后的值因此它比 new() 进行更多的工作详见[第 7.2.3/4 节](file:///D:/self/资料/go/the-way-to-go/eBook/07.2.md)、[第 8.1.1 节](file:///D:/self/资料/go/the-way-to-go/eBook/08.1.md)和[第 14.2.1 节](file:///D:/self/资料/go/the-way-to-go/eBook/14.2.md)。new() 是一个函数不要忘记它的括号
copy()append()用于复制和连接切片
panic()recover()两者均用于错误处理机制
print()println()底层打印函数详见[第 4.2 节](file:///D:/self/资料/go/the-way-to-go/eBook/04.2.md)在部署环境中建议使用 fmt
complex()real ()imag()用于创建和操作复数详见[第 4.5.2.2 节](file:///D:/self/资料/go/the-way-to-go/eBook/04.5.md)

将函数作为参数

回调函数可以作为其它函数的参数进行传递然后在其它函数内调用执行

package main

import (
	"fmt"
)

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它的返回值是字符串 s 中第一个使函数 f(c) 返回 true 的 Unicode 字符的索引值。如果找不到则返回 -1

例如 strings.IndexFunc(line, unicode.IsSpace) 就会返回 line 中第一个空白字符的索引值。当然您也可以书写自己的函数

func IsAscii(c int) bool {
	if c > 255 {
		return false
	}
	return true
}

闭包

不想给函数起名字则可以使用匿名函数

此函数不能独立存在但可以被赋值于某个变量当中保存函数的地址再通过变量名对函数进行调用也可以直接对匿名函数进行调用

//保存函数地址并进行使用
fplus:=func(x,y int) int {return x +y }
fplus(3,4)
//直接调用
func(x,y int) int {return x + y} (3,4)
//计算1到100万整数的总和
func() {
	sum := 0
	for i := 1; i <= 1e6; i++ {
		sum += i
	}
}()

表示参数列表的第一对括号必须紧挨着关键字 func因为匿名函数没有名称。花括号 {} 涵盖着函数体最后的一对括号表示对该匿名函数的调用。

实例

package main

import "fmt"

func main() {
	f()
}
func f() {
	for i := 0; i < 4; i++ {
		g := func(i int) { fmt.Printf("%d ", i) }
		g(i)
		fmt.Printf(" - g is of type %T and has value %v\n", g, g)
	}
}

我们可以看到变量 g 代表的是 func(int)变量的值是一个内存地址

image-20231022204828753

关键字 defer 经常配合匿名函数使用它可以用于改变函数的命名返回值。

匿名函数还可以配合 go 关键字来作为 goroutine 使用

匿名函数同样被称之为闭包函数式语言的术语它们被允许调用定义在其它环境下的变量。闭包可使得某个函数捕捉到一些外部状态例如函数被创建时的状态。另一种表示方式为一个闭包继承了函数所声明时的作用域。这种状态作用域内的变量都被共享到闭包的环境中因此这些变量可以在闭包中被操作直到被销毁

闭包经常被用作包装函数它们会预先定义好 1 个或多个参数以用于包装详见下一节中的示例。另一个不错的应用就是使用闭包来完成更加简洁的错误检查

闭包将函数作为返回值

函数 Adder() 现在被赋值到变量 f 中类型为 func(int) int

package main

import "fmt"

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
	}
}

输出

image-20231022205637298

在多次调用中变量 x 的值是被保留的即 0 + 1 = 1然后 1 + 20 = 21最后 21 + 300 = 321闭包函数保存并积累其中的变量的值不管外部函数退出与否它都能够继续操作外部函数中的局部变量。

这些局部变量同样可以是参数

这样闭包函数就能够被应用到整个集合的元素上并修改它们的值。然后这些变量就可以用于表示或计算全局或平均值。

一个返回值为另一个函数的函数可以被称之为工厂函数这在您需要创建一系列相似的函数的时候非常有用书写一个工厂函数而不是针对每种情况都书写一个函数。下面的函数演示了如何动态返回追加后缀的函数

func MakeAddSuffix(suffix string) func(string) string {
	return func(name string) string {
		if !strings.HasSuffix(name, suffix) {
			return name + suffix
		}
		return name
	}
}

现在我们可以生成如下函数

addBmp := MakeAddSuffix(".bmp")
addJpeg := MakeAddSuffix(".jpeg")

然后调用它们

addBmp("file") // returns: file.bmp
addJpeg("file") // returns: file.jpeg

可以返回其它函数的函数和接受其它函数作为参数的函数均被称之为高阶函数是函数式语言的特点。我们已经在中得知函数也是一种值因此很显然 Go 语言具有一些函数式语言的特性。闭包在 Go 语言中非常常见常用于 goroutine 和管道操作。

我们将会看到 Go 语言中的函数在处理混合对象时的强大能力。

计算函数执行时间

有时候能够知道一个计算执行消耗的时间是非常有意义的尤其是在对比和基准测试中。最简单的一个办法就是在计算开始之前设置一个起始时间再记录计算结束时的结束时间最后计算它们的差值就是这个计算所消耗的时间。想要实现这样的做法可以使用 time 包中的 Now()Sub() 函数

start := time.Now()
longCalculation()
end := time.Now()
delta := end.Sub(start)
fmt.Printf("longCalculation took this amount of time: %s\n", delta)

使用内存缓存来提升性能

类似于前缀和

内存缓存的技术在使用计算成本相对昂贵的函数时非常有用不仅限于例子中的递归譬如大量进行相同参数的运算。这种技术还可以应用于纯函数中即相同输入必定获得相同输出的函数。

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: go

“[Golang]多返回值函数、defer关键字、内置函数、变参函数、类成员函数、匿名函数-CSDN博客” 的相关文章