GO语言 函数

函数 Golang语言

函数

1 函数定义

在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编译器会自动进行推断。

2 值传递与引用传递

因为在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语言通过函数名字的大小写来显示函数的可见性,小写字母开头的函数只在本包内可见,大写字母开头的函数才能被其他包使用。
这个规则也适用于类型和变量的可见性。

3 变长参数

在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[:]...)
}

既然变参时切片,那么参数复制的仅是切片本身,并不包括底层数组,也因此可修改原数据。

4 多返回值

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

}

5命名返回值

除了上面支持的多返回值,在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 匿名函数

匿名函数如其名字一样,是一个没有名字的函数,除了没有名字外其他地方与正常函数相同。匿名函数可以直接调用,保存到变量,作为参数或者返回值。

6:

func main() {
	f := func() string {
		return "hello world"
	}
	fmt.Println(f())
}
匿名函数

匿名函数是指没有定义名字符号的函数。

除了没有名字外,在函数内部定义匿名函数可以形成嵌套效果。匿名函数可直接调用,保存到变量,作为参数或返回值。

func main(){
    func(s string){
        fmt.Println(s)
    }("hellow world")
}

除闭包因素外,匿名函数也是常见的重构的手段。可将大函数分解成多个相对独立的匿名函数块,然后相对简洁的完成调用逻辑流程,实现框架和细节分离。

7 闭包

闭包可以解释为一个函数与这个函数外部变量的一个封装。粗略的可以理解为一个类,类里面有变量和方法,其中闭包所包含的外部变量对应着类中的静态变量。 为什么这么理解,首先让我们来看一个例子。

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 intfunc(){
    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())
       } }

你可能感兴趣的:(go,go,编程语言)