在go语言中函数定义格式如下:
func functionName([parameter list]) [returnTypes]{
//body
}
函数由func关键字进行声明。
functionName:代表函数名。
parameter list:代表参数列表,函数的参数是可选的,可以包含参数也可以不包含参数。
returnTypes:返回值类型,返回值是可选的,可以有返回值,也可以没有返回值。
body:用于写函数的具体逻辑
函数构成代码执行的基本逻辑结构。在Go语言中,函数的基本组成为:关键字func、函数名、参数列表、返回值、函数体和返回语句。
func add(a int,b int)(ret int,err error){
if (a < 0 || b < 0){
err = errors.New("should be non-negative number!")
return
}
return a+b,nil
}
如果参数列表中的若干相邻的参数类型相同,如上例中的a和b,则可以在参数列表中省略前面变量的类型声明:
func add(a,b int)(ret int,err error){
///...
}
Go语言函数有一些限制:
函数中,左花括号不能另起一行。
如:
func test()
{ //错误,Go语言规定函数左括号不能在新的一行开头
}
Go语言中函数不能嵌套:
func main(){
func test(a,b int ,err error){
//错误,函数不支持嵌套操作
...
}
}
例1:
下面的函数是用于求两个数的和
func GetSum(num1 int, num2 int) int {
result := num1 + num2
return result
}
这个函数传递了两个参数,分别为num1与num2,并且他们都为int类型,将相加后的结果进行返回。
上面这个函数还可以这样定义
func GetSum1(num1, num2 int) int {
result := num1 + num2
return result
}
当num1和num2是相同类型的时候我们可以省略掉前面的类型,go编译器会自动进行推断。
因为在go语言中存在值类型与引用类型,所以在函数参数进行传递时也要注意这个问题。
例2:
func paramFunc(a int, b *int, c []int) {
a = 100
*b = 100
c[1] = 100
fmt.Println("paramFunc:")
fmt.Println(a)
fmt.Println(*b)
fmt.Println(c)
}
func main() {
a := 1
b := 1
c := []int{1, 2, 3}
paramFunc(a, &b, c)
fmt.Println("main:")
fmt.Println(a)
fmt.Println(b)
fmt.Println(c)
}
程序输出如下
paramFunc:
100
100
[1 100 3]
main:
1
100
[1 100 3]
Go语言函数调用只需要导入该函数所在的包,直接调用:
import "mymath"
c:=mymath.add(1,2)
Go语言通过函数名字的大小写来显示函数的可见性,小写字母开头的函数只在本包内可见,大写字母开头的函数才能被其他包使用。
这个规则也适用于类型和变量的可见性。
在go语言中也支持变长参数,但需要注意的是变长参数必须放在函数参数的最后一个,否则会报错。
下面这段代码演示了如何使用变长参数
例3:
func main() {
slice := []int{7, 9, 3, 5, 1}
x := min(slice...)
fmt.Printf("The minimum 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
}
当然上面这段代码直接将切片作为参数也能实现同样的效果,但是变长参数更多的是为了参数不确定的情况,例如fmt包中的Printf函数设计如下:
func Printf(format string, a ...interface{}) (n int, err error) {
return Fprintf(os.Stdout, format, a...)
}
上面这段代码暂时看不懂也没关系,但是只需要记住,当你想传递给函数的参数不能确定有多少时可以使用变长参数。
Go语言不支持默认值的可选参数,不支持实名实参。调用时,必须按签名顺序指定类型和数量实参,就算以”_”命名的参数也不能忽略。
func test(x,y int,s string,_ bool){
return nil
}
func main(){
test(1,2,"abc")
//错误,"_"命名的参数也不能忽略
}
参数可视作函数局部变量,因此不能在相同的层次定义同名变量。
func add(x,y int) int{
x:=100 //错误
...
}
不管是指针、引用类型,还是其他类型参数,都是值拷贝传递。区别无非是拷贝目标对象,还是拷贝指针而已。在函数调用前,会为形参和返回值分配内存空间,并将实参拷贝到形参内存。
被复制的指针会延长目标对象的生命周期,还有可能导致它被分配到堆上。在栈上复制小对象只需要很少的指令,比运行时堆内存分配快。在并发编程的时候,尽量使用不可变对象,可以消除数据同步等麻烦。如果复制成本过高,或者需要修改原状态,直接使用指针更好。
变参本质上是一个切片,只能接受一到多个同类型参数,且必须放在列表尾部:
func test(s string,a ...int){
...
}
将切片作为变参时,须进行展开操作。如果是数组,必将其转换成切片。
func test(a ...int){
fmt.Println(a)
}
func main(){
a := [3]int{10,20,30}
test(a[:]...)
}
既然变参时切片,那么参数复制的仅是切片本身,并不包括底层数组,也因此可修改原数据。
go语言中函数还支持一个特性那就是:多返回值。通过返回结果与一个错误值,这样可以使函数的调用者很方便的知道函数是否执行成功,这样的模式也被称为command,ok模式,在我们未来的程序设计中也推荐大家使用这种方式。下面这段代码显示了如何操作多返回值。
例4:
func div(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("The divisor cannot be zero.")
}
return a / b, nil
}
func main() {
result, err := div(1, 2)
if err != nil {
fmt.Printf("error: %v", err)
return
}
fmt.Println("result: ", result)
}
也可以下面这种模式
func main() {
if result, err := div(1, 2); err != nil {
fmt.Printf("error: %v", err)
return
} else {
fmt.Println("result: ", result)
}
}
注:多返回值需要使用()进行标记。
有返回值的函数,必须有明确的return终止语句。
函数可以返回多值模式,函数可以返回更多状态,尤其是error模式。
func dic(x,y int)(int,error){
if y==0 {
return 0,errors.New("division by zero")
}
return x/y,nil
}
除了上面支持的多返回值,在go语言中还可以给返回值命名,当需要返回的时候,我们只需要一条简单的不带参数的return语句。我们将上面那个除法的函数修改一下
例5:
func div(a, b float64) (result float64, err error) {
if b == 0 {
return 0, errors.New("被除数不能等于0")
}
result = a / b
return
}
注:即使只有一个命名返回值,也需要使用()括起来。
命名返回值让函数声明更加清晰,同时也会改善帮助文档和代码编辑器提示。
命名返回值和参数一样,可当做函数局部变量使用,最后由return隐式返回。
func dic(x,y int)(z int,err error){
if y==0 {
err=errors.New("division by zero")
return
}
z = x/y
return
}
命名返回值会被不同层级的同名变量屏蔽。编译器可以检查这类错误,只需要显示return返回即可。
func add(x,y int)(z int){
z:= x + y //同名局部变量进行了覆盖
return //错误,改成return z
}
如果使用命名返回值,则需要全部使用命名返回值。
func test()(int,s string){
//错误
...
}
匿名函数如其名字一样,是一个没有名字的函数,除了没有名字外其他地方与正常函数相同。匿名函数可以直接调用,保存到变量,作为参数或者返回值。
例6:
func main() {
f := func() string {
return "hello world"
}
fmt.Println(f())
}
匿名函数是指没有定义名字符号的函数。
除了没有名字外,在函数内部定义匿名函数可以形成嵌套效果。匿名函数可直接调用,保存到变量,作为参数或返回值。
func main(){
func(s string){
fmt.Println(s)
}("hellow world")
}
除闭包因素外,匿名函数也是常见的重构的手段。可将大函数分解成多个相对独立的匿名函数块,然后相对简洁的完成调用逻辑流程,实现框架和细节分离。
闭包可以解释为一个函数与这个函数外部变量的一个封装。粗略的可以理解为一个类,类里面有变量和方法,其中闭包所包含的外部变量对应着类中的静态变量。 为什么这么理解,首先让我们来看一个例子。
例7:
func add() func(int) int {
n := 10
str := "string"
return func(x int) int {
n = n + x
str += strconv.Itoa(x)
fmt.Print(str, " ")
return n
}
}
func main() {
f := add()
fmt.Println(f(1))
fmt.Println(f(2))
fmt.Println(f(3))
f = add()
fmt.Println(f(1))
fmt.Println(f(2))
fmt.Println(f(3))
}
程序输出结果如下:
string1 11
string12 13
string123 16
string1 11
string12 13
string123 16
如果不了解的闭包肯定会觉得很奇怪,为什么会输出这样的结果。这就要用到我最开始的解释。闭包就是一个函数和一个函数外的变量的封装,而且这个变量就对应着类中的静态变量。 这样就可以将这个程序的输出结果解释的通了。
最开始我们先声明一个函数add,在函数体内返回一个匿名函数
其中的n,str与下面的匿名函数构成了整个的闭包,n与str就像类中的静态变量只会初始化一次,所以说尽管后面多次调用这个整体函数,里面都不会再重新初始化了
而且对于外部变量的操作是累加的,这与类中的静态变量也是一致的
在go语言学习笔记中,雨痕提到在汇编代码中,闭包返回的不仅仅是匿名函数,还包括所引用的环境变量指针,这与我们之前的解释也是类似的,闭包通过操作指针来调用对应的变量。
Go语言匿名函数就是一个闭包,闭包是可以包含自由变量(未绑定到特定变量)的代码块,这些变量不在这个代码块内或者任何全局上下文中定义,而是在定义代码块的环境中定义。要执行的代码块(由于自由变量包含在代码块中,所以这些自由变量以及他们引用的对象没有被释放)为自由变量提供绑定的计算环境(作用域)。
func test(x int)func(){
return func(){
println(x)
}
}
func main(){
f := test(123)
f()
}
test返回的匿名函数会引用上下文环境变量x,当main函数执行时,依旧可以读取到x的值。
正因为闭包通过指针引用环境变量,可能导致其生命周期延长,甚至被分配到堆内存。另外,还有”延迟求值”特性。
func test()[]func(){
var s []func()
for i := 0;i < 2;i++{
s = append(s,func(){
fmt.Println(i)
})
}
return s
}
func main(){
for _,f := rang test(){
f()
}
}
结果是:
2
2
在for循环内部复用局部变量i,每次添加的匿名函数引用的是同意变量。添加仅仅把匿名函数放入列表,并未执行。当main函数执行这些函数时,读取的是环境变量i最后循环的值。
修改为:
func test()[]func(){
var s []func()
for i := 0;i < 2;i++{
x := i
//每次用不同的环境变量或传参复制,让各自的闭包环境各不相同。
s = append(s,func(){
fmt.Println(x)
})
}
return s
}
the way to go小练习——闭包实现斐波那契数列
func fib() func() int {
var a int = 0
var b int = 1
return func() int {
c := a
a = b
b = a + c
return c
} }
func main() {
f := fib()
for i := 0; i < 10; i++ {
fmt.Println(f())
} }