Go的研习笔记-day5(以Java的视角学习Go)

函数:也就是Go里面的基本代码块,与Java中的方法类似

Go是编译型语言,所以函数编写的顺序是无关紧要的;鉴于可读性的需求,最好把 main() 函数写在文件的前面,其他函数按照一定逻辑顺序进行编写(例如函数被调用的顺序)。
编写多个函数的主要目的是将一个需要很多行代码的复杂问题分解为一系列简单的任务(那就是函数)来解决。而且,同一个任务(函数)可以被调用多次,有助于代码重用。
简单的 return 语句也可以用来结束 for 死循环,或者结束一个协程(goroutine)。
Go 里面有三种类型的函数:

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

除了main()、init()函数外,其它所有类型的函数都可以有参数与返回值。函数参数、返回值以及它们的类型被统称为函数签名。
函数被调用的基本格式如下:
pack1.Function(arg1, arg2, …, argn)
Function 是 pack1 包里面的一个函数,括号里的是被调用函数的实参(argument):这些值被传递给被调用函数的形参(parameter)。函数被调用的时候,这些实参将被复制(简单而言)然后传递给被调用函数。函数一般是在其他函数里面被调用的,这个其他函数被称为调用函数(calling function)。函数能多次调用其他函数,这些被调用函数按顺序(简单而言)执行,理论上,函数调用其他函数的次数是无穷的(直到函数调用栈被耗尽)。这里表明也会出现Java中的栈溢出错误。
函数重载(function overloading)指的是可以编写多个同名函数,只要它们拥有不同的形参与/或者不同的返回值,在 Go 里面函数重载是不被允许的。这将导致一个编译错误:
funcName redeclared in this book, previous declaration at lineno
这里与Java中重载方法不同需要注意
Go 语言不支持这项特性的主要原因是函数重载需要进行多余的类型匹配影响性能;没有重载意味着只是一个简单的函数调度。所以你需要给不同的函数使用不同的名字。如果需要申明一个在外部定义的函数,你只需要给出函数名与函数签名,不需要给出函数体:
func flushICache(begin, end uintptr) // implemented externally
函数也可以以申明的方式被使用,作为一个函数类型,就像:
type binOp func(int, int) int
在这里,不需要函数体 {}。
函数是一等值(first-class value):它们可以赋值给变量,就像 add := binOp 一样。
这个变量知道自己指向的函数的签名,所以给它赋一个具有不同签名的函数值是不可能的。
函数值(functions value)之间可以相互比较:如果它们引用的是相同的函数或者都是 nil 的话,则认为它们是相同的函数。函数不能在其它函数里面声明(不能嵌套),不过我们可以通过使用匿名函数来破除这个限制。
本来go没有泛型概念,但是现在也将支持。

  • 函数参数与返回值
    函数能够接收参数供自己使用,也可以返回零个或多个值(我们通常把返回多个值称为返回一组值)。相比与 C、C++、Java 和 C#,多值返回是 Go 的一大特性,为我们判断一个函数是否正常执行提供了方便。
    我们通过 return 关键字返回一组值。事实上,任何一个有返回值(单个或多个)的函数都必须以 return 或 panic结尾。
    在函数块里面,return 之后的语句都不会执行。如果一个函数需要返回值,那么这个函数里面的每一个代码分支(code-path)都要有 return 语句。
    函数定义时,它的形参一般是有名字的,不过我们也可以定义没有形参名的函数,只有相应的形参类型,就像这样:func f(int, int, float64)。
    没有参数的函数通常被称为 niladic 函数(niladic function),就像 main.main()。
  • 按值传递(call by value) 按引用传递(call by reference)
  • Go 默认使用按值传递来传递参数,也就是传递参数的副本。函数接收参数副本之后,在使用变量的过程中可能对副本的值进行更改,但不会影响到原来的变量,比如 Function(arg1)。
    如果你希望函数可以直接修改参数的值,而不是对参数的副本进行操作,你需要将参数的地址(变量名前面添加&符号,比如 &variable)传递给函数,这就是按引用传递,比如 Function(&arg1),此时传递给函数的是一个指针。如果传递给函数的是一个指针,指针的值(一个地址)会被复制,但指针的值所指向的地址上的值不会被复制;我们可以通过这个指针的值来修改这个值所指向的地址上的值。(译者注:指针也是变量类型,有自己的地址和值,通常指针的值指向一个变量的地址。所以,按引用传递也是按值传递。)
    几乎在任何情况下,传递指针(一个32位或者64位的值)的消耗都比传递副本来得少。
    在函数调用时,像切片(slice)、字典(map)、接口(interface)、通道(channel)这样的引用类型都是默认使用引用传递(即使没有显式的指出指针)。
    有些函数只是完成一个任务,并没有返回值。我们仅仅是利用了这种函数的副作用,就像输出文本到终端,发送一个邮件或者是记录一个错误等。
    但是绝大部分的函数还是带有返回值的。
  • 命名的返回值(named return variables)
    尽量使用命名返回值:会使代码更清晰、更简短,同时更加容易读懂。
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
}
  • 空白符
    空白符用来匹配一些不需要的值,然后丢弃掉
package main

import "fmt"

func main() {
    var i1 int
    var f1 float32
    i1, _, f1 = ThreeValues()
    fmt.Printf("The int: %d, the float: %f \n", i1, f1)
}

func ThreeValues() (int, int, float32) {
    return 5, 6, 7.5
}
输出结果:

The int: 5, the float: 7.500000
  • 改变外部变量
    传递指针给函数不但可以节省内存(因为没有复制变量的值),而且赋予了函数直接修改外部变量的能力,所以被修改的变量不再需要使用 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
}
  • 传递变长参数
    在Java中变长参数和go语言中一样都是通过…来定义的
    func myFunc(a, b, arg …int) {}
    这个函数接受一个类似某个类型的 slice 的参数
    func Greeting(prefix string, who …string)
    Greeting(“hello:”, “Joe”, “Anna”, “Eileen”)
    在 Greeting 函数中,变量 who 的值为 []string{“Joe”, “Anna”, “Eileen”}。
    如果参数被存储在一个 slice 类型的变量 slice 中,则可以通过 slice… 的形式来传递参数,调用变参函数。
    如果变长参数的类型并不是都相同的,假设现在使用 5 个参数来进行传递,有 2 种方案可以解决这个问题:
  • 使用结构
    定义一个结构类型,假设它叫 Options,用以存储所有可能的参数:
type Options struct {
	par1 type1,
	par2 type2,
	...
}
  • 使用空接口:
    如果一个变长参数的类型没有被指定,则可以使用默认的空接口 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 的用法类似于面向对象编程语言 Java 和 C# 的 finally 语句块,它一般用于释放某些已分配的资源。
    当有多个 defer 行为被注册时,它们会以逆序执行(类似栈,即后进先出):
func f() {
	for i := 0; i < 5; i++ {
		defer fmt.Printf("%d ", i)
	}
}
上面的代码将会输出:4 3 2 1 0。
关键字 defer 允许我们进行一些函数执行完成后的收尾工作,例如:
关闭文件流 
// open a file  
defer file.Close()
解锁一个加锁的资源 
mu.Lock()  
defer mu.Unlock() 
打印最终报告
printHeader()  
defer printFooter()
关闭数据库链接
// open a database connection  
defer disconnectFromDB()
合理使用 defer 语句能够使得代码更加简洁。
  • 使用 defer 语句实现代码追踪
    一个基础但十分实用的实现代码执行追踪的方案就是在进入和离开某个函数打印相关的消息,即可以提炼为下面两个函数:
    func trace(s string) { fmt.Println(“entering:”, s) }
    func untrace(s string) { fmt.Println(“leaving:”, s) }
  • 使用 defer 语句来记录函数的参数与返回值
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")
}

内置函数
Go 语言拥有一些不需要进行导入操作就可以使用的内置函数。它们有时可以针对不同的类型进行操作,例如:len、cap 和 append,或必须用于系统级的操作,例如:panic。因此,它们需要直接获得编译器的支持。
在这里插入图片描述
递归函数
栈溢出:一般出现在大量的递归调用导致的程序栈内存分配耗尽。这个问题可以通过一个名为懒惰求值的技术解决,在 Go 语言中,我们可以使用管道(channel)和 goroutine来实现。通过这个方案也可以优化斐波那契数列的生成问题。

将函数作为参数
函数可以作为其它函数的参数进行传递,然后在其它函数内调用执行,一般称之为回调。下面是一个将函数作为参数的简单例子(function_parameter.go):
将函数作为参数的最好的例子是函数 strings.IndexFunc():
该函数的签名是 func IndexFunc(s string, f func(c rune) bool) int,它的返回值是在函数 f© 返回 true、-1 或从未返回时的索引值。
例如 strings.IndexFunc(line, unicode.IsSpace) 就会返回 line 中第一个空白字符的索引值

闭包
当我们不希望给函数起名字的时候,可以使用匿名函数,例如:func(x, y int) int { return x + y }。
这样的一个函数不能够独立存在(编译器会返回错误:non-declaration statement outside function body),但可以被赋值于某个变量,即保存函数的地址到变量中:fplus := func(x, y int) int { return x + y },然后通过变量名对函数进行调用:fplus(3,4)。
当然,您也可以直接对匿名函数进行调用:func(x, y int) int { return x + y } (3, 4)。
下面是一个计算从 1 到 1 百万整数的总和的匿名函数:

func() {
	sum := 0
	for i := 1; i <= 1e6; i++ {
		sum += i
	}
}()

defer 语句和匿名函数
关键字 defer 经常配合匿名函数使用,它可以用于改变函数的命名返回值。
匿名函数还可以配合 go 关键字来作为 goroutine 使用
匿名函数同样被称之为闭包(函数式语言的术语):它们被允许调用定义在其它环境下的变量。闭包可使得某个函数捕捉到一些外部状态,例如:函数被创建时的状态。另一种表示方式为:一个闭包继承了函数所声明时的作用域。这种状态(作用域内的变量)都被共享到闭包的环境中,因此这些变量可以在闭包中被操作,直到被销毁。闭包经常被用作包装函数:它们会预先定义好 1 个或多个参数以用于包装,详见下一节中的示例。另一个不错的应用就是使用闭包来完成更加简洁的错误检查

应用闭包:将函数作为返回值
在闭包中使用到的变量可以是在闭包函数体内声明的,也可以是在外部函数声明的:

 var g int
go func(i int) {
	s := 0
	for j := 0; j < i; j++ { s += j }
	g = s
}(1000) // Passes argument 1000 to the function literal.
这样闭包函数就能够被应用到整个集合的元素上,并修改它们的值。然后这些变量就可以用于表示或计算全局或平均值。

理解以下程序的工作原理:
一个返回值为另一个函数的函数可以被称之为工厂函数,这在您需要创建一系列相似的函数的时候非常有用:书写一个工厂函数而不是针对每种情况都书写一个函数。下面的函数演示了如何动态返回追加后缀的函数:
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 语言中的函数在处理混合对象时也有强大能力。

使用闭包调试
当您在分析和调试复杂的程序时,无数个函数在不同的代码文件中相互调用,如果这时候能够准确地知道哪个文件中的具体哪个函数正在执行,对于调试是十分有帮助的。您可以使用 runtime 或 log 包中的特殊函数来实现这样的功能。包 runtime 中的函数 Caller() 提供了相应的信息,因此可以在需要的时候实现一个 where() 闭包函数来打印函数执行的位置:

where := func() {
	_, file, line, _ := runtime.Caller(1)
	log.Printf("%s:%d", file, line)
}
where()
// some code
where()
// some more code
where()

计算函数执行时间
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)

通过内存缓存来提升性能
当在进行大量的计算时,提升性能最直接有效的一种方式就是避免重复计算。通过在内存中缓存和重复利用相同计算的结果,称之为内存缓存。

发布了213 篇原创文章 · 获赞 258 · 访问量 28万+

猜你喜欢

转载自blog.csdn.net/wolf_love666/article/details/98212135