函数返回多值

这个在java中是不支持的,得定义一个Tuple去接收,这个功能还是很有用的

  1. package main
  2. import "fmt"
  3. func swap(x, y string) (string, string) {
  4. return y, x
  5. }
  6. func main() {
  7. a, b := swap("Google", "Runoob")
  8. fmt.Println(a, b)
  9. }

函数参数可以是值传递也可以是引用传递

值传递:值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。

  1. /* 定义相互交换值的函数 */
  2. func swap(x, y int) int {
  3. var temp int
  4. temp = x /* 保存 x 的值 */
  5. x = y /* 将 y 值赋给 x */
  6. y = temp /* 将 temp 值赋给 y*/
  7. return temp;
  8. }
  9. swap(a, b)

引用传递:引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

  1. /* 定义交换值函数*/
  2. func swap(x *int, y *int) {
  3. var temp int
  4. temp = *x /* 保持 x 地址上的值 */
  5. *x = *y /* 将 y 值赋给 x */
  6. *y = temp /* 将 temp 值赋给 y */
  7. }
  8. swap(&a, &b) 传入的是ab的值地址

这两个取决于函数定义时的接受参数的类型是不是引用类型

函数变量

函数在Go语言中是头等重要的值:就像其他的值,函数变量也有类型,而且它们可以赋给变量

  1. package main
  2. import (
  3. "fmt"
  4. "math"
  5. )
  6. func main(){
  7. /* 声明函数变量 */
  8. getSquareRoot := func(x float64) float64 {
  9. return math.Sqrt(x)
  10. }
  11. /* 使用函数 */
  12. fmt.Println(getSquareRoot(9))
  13. }

函数作为参数传递,实现回调

  1. package main
  2. import "fmt"
  3. // 声明一个函数类型
  4. type cb func(int) int
  5. func main() {
  6. testCallBack(1, callBack) // 这个是下面声明的callback函数
  7. testCallBack(2, func(x int) int { // 类似java中的匿名函数
  8. fmt.Printf("我是回调,x:%d\n", x)
  9. return x
  10. })
  11. }
  12. func testCallBack(x int, f cb) {
  13. f(x)
  14. }
  15. func callBack(x int) int {
  16. fmt.Printf("我是回调,x:%d\n", x)
  17. return x
  18. }

闭包

Go 语言支持匿名函数,可作为闭包。匿名函数是一个”内联”语句或表达式。匿名函数的优越性在于可以直接使用函数内的变量,不必申明。

以下实例中,我们创建了函数 getSequence() ,返回另外一个函数。该函数的目的是在闭包中递增 i 变量,代码如下:

  1. package main
  2. import "fmt"
  3. func getSequence() func() int {
  4. i:=0
  5. return func() int {
  6. i+=1
  7. return i
  8. }
  9. }
  10. func main(){
  11. /* nextNumber 为一个函数,函数 i 为 0 */
  12. nextNumber := getSequence()
  13. /* 调用 nextNumber 函数,i 变量自增 1 并返回 */
  14. fmt.Println(nextNumber())
  15. fmt.Println(nextNumber())
  16. fmt.Println(nextNumber())
  17. /* 创建新的函数 nextNumber1,并查看结果 */
  18. nextNumber1 := getSequence()
  19. fmt.Println(nextNumber1())
  20. fmt.Println(nextNumber1())
  21. }

执行结果

  1. 1
  2. 2
  3. 3
  4. 1
  5. 2
  1. func square() func() int {
  2. var x int
  3. return func() int {
  4. x++
  5. return x * x
  6. }
  7. }
  8. func main() {
  9. f := square()
  10. fmt.Println(f())
  11. fmt.Println(f())
  12. fmt.Println(f())
  13. fmt.Println(f())
  14. fmt.Println(f())
  15. }
  16. 1
  17. 4
  18. 9
  19. 16
  20. 25

以这种匿名方式定义的函数能够获得到整个词法环境,因此里层函数可以使用外层函数中的变量。函数square返回了另一个函数,类型是 func() int 。调用square创建了一个局部变量x而且返回了一个匿名函数,每次调用square都会递增x值,然后返回x的平方。第二次调用square函数将创建第二个变量x,然后返回一个递增x值的新匿名函数。这个例子延时函数变量不仅是一段代码,还可以拥有状态。里层的匿名函数能够获取和更新外层函数的局部变量。这些隐藏的变量引用就是我们把函数归类为引用类型而且函数变量无法进行比较的原因。函数变量类似于使用闭包方法实现的变量,Go程序员通常把函数变量成为闭包。
我们再一次看这个例子里面变量的生命周期不是由他的作用域决定的:变量x在main函数中返回的square函数后依旧存在(这时候x是隐藏在函数变量f中了)

错误

error是内置的接口类型,Go语言通过普通的值而非异常来报告错误,尽管Go语言有异常机制,但是Go语言的异常只是针对程序bug导致的预料外的错误,而不能作为常规的错误处理的方法
这样做的原因是异常会陷入带有错误消息的控制流中处理它,通常会导致预期外的结果:错误会以难以理解的栈跟踪信息报告给用户,这些信息大都是关于程序结构方面而不是简单明了的错误信息。
相比之下,Go程序使用通常的控制流机制(if return)应对错误。

递归

许多编程语言使用固定长度的函数调用栈;大小在64KB到2MB之间。递归的深度受限于固定长度的栈大小,所以当进行深度递归调用时,必须谨防栈溢出。而go语言的实现使用了可变长的栈,栈的大小随着使用而增长,可达到1GB左右的上限。

捕获迭代变量

假设有一个程序必须创建一系列的目录之后又会删除他们,可以使用一个包含函数变量的slice进行清理操作

  1. func main() {
  2. tempDirs := []string{"/tmp/xiuixiu", "/tmp/c"}
  3. var rmdirs []func()
  4. for _, d := range tempDirs {
  5. dir := d // 这一行是必须的
  6. os.MkdirAll(dir, 0755)
  7. rmdirs = append(rmdirs, func() {
  8. os.RemoveAll(dir)
  9. })
  10. }
  11. for _, rmdir := range rmdirs {
  12. rmdir()
  13. }
  14. }

在这里也许会对这一步操作奇怪,为什么要多这一步赋值操作呢?

  1. dir := d

这个原因是因为d变量是在for循环创建的,d变量在循环中会不断更新,而在循环里创建的所有函数变量都共享同一个变量(存储位置),因此在调用清理函数时,只会清理最后一个创建的目录,导致错误。这个时候就需要像以上的
程序一样引入一个内部变量来解决这个问题。

变长函数

变长函数在被调用的时候可以有可变的参数个数,在参数列表的最后类型名称之前使用省略号,表示声明一个变长函数,调用这个函数的时候可以传递该类型任意数目的参数

延迟函数调用

一个defer语句就是一个普通的函数或方法调用,在调用之前加上关键词defer。函数和参数表达式在语句执行时求职,但无论是正常情况下,执行return语句或者函数执行完毕,还是在不正常的请款修改,比如发生宕机,实际的调用推迟到包含defer语句的函数结束之后才执行。defer语句没有限制使用次数;执行的时候以调用defer语句顺序的倒序进行

defer语句常用来进行资源的释放,正确使用defer语句的地方实在成功获得资源之后。通常在打开文件,创建连接,获取锁之后。

defer语句也可以用用来调试一个复杂函数,即在函数的入口和出口的地方设置调试行为,比如可以获取函数的执行耗时。

因为defer语句是在return语句之后执行,并且可以更新函数的结果变量。因为匿名函数可以得到外层函数作用域内的变量,所以延迟执行的匿名函数可以观察到函数的返回结果
**

方法

在Go语言中同时有函数和方法,函数是指不属于任何结构体、类型的方法,也就是说,函数是没有接收者的;而方法是有接收者的,我们说的方法要么是属于一个结构体,要么是属于一个新定义的类型。类比java,java是面向对象的语言,其实只有方法,没有函数这一说,要类比的话,go中的函数可能类似于java中的静态方法,而go中的方法则类似于java中的成员方法。
一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。所有给定类型的方法属于该类型的方法集。

语法格式如下:

  1. func (variable_name variable_data_type) function_name() [return_type]{
  2. /* 函数体*/
  3. }

方法的声明和函数类似,他们的区别是:方法在定义的时候,会在func和方法名之间增加一个参数,这个参数就是接收者,这样我们定义的这个方法就和接收者绑定在了一起,称之为这个接收者的方法。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. /* 定义结构体 */
  6. type Circle struct {
  7. radius float64
  8. }
  9. func main() {
  10. var c1 Circle
  11. c1.radius = 10.00
  12. fmt.Println("圆的面积 = ", c1.getArea())
  13. }
  14. //该 method 属于 Circle 类型对象中的方法
  15. func (c Circle) getArea() float64 {
  16. //c.radius 即为 Circle 类型对象中的属性
  17. return 3.14 * c.radius * c.radius
  18. }

Go 没有面向对象,而我们知道常见的 Java。

C++ 等语言中,实现类的方法做法都是编译器隐式的给函数加一个 this 指针,而在 Go 里,这个 this 指针需要明确的申明出来,其实和其它 OO 语言并没有很大的区别。

在 C++ 中是这样的:

  1. class Circle {
  2. public:
  3. float getArea() {
  4. return 3.14 * radius * radius;
  5. }
  6. private:
  7. float radius;
  8. }
  9. // 其中 getArea 经过编译器处理大致变为
  10. float getArea(Circle *const c) {
  11. ...
  12. }

在Go中则是如下,在这样的实现中,其实就省略掉了this指针:

  1. func (c Circle) getArea() float64 {
  2. //c.radius 即为 Circle 类型对象中的属性
  3. return 3.14 * c.radius * c.radius
  4. }

Go和许多其他面向对象的语言不通,它可以将方法绑定到任何类型上。可以很方便地为简单的类型(如数字,字符串,slice,map甚至函数)定义附加的行为。同一个包下的任何类型都可以声明方法,只要它的类型不是指针类型也不是接口类型

  1. type Point struct { X, Y float64}
  2. func (p Point) Distance(q Point) float64 {
  3. return math.Hypot(q.X-p.X, q.Y-p.Y)
  4. }
  5. type Path []Point
  6. func (path Path) Distance() float64 {
  7. sum := 0.0
  8. for i := range path {
  9. if i > 0 {
  10. sum += path[i-1].Distance(path[i]) // 前面有为point类型定义的Path方法
  11. }
  12. }
  13. }
  14. perim := Path {
  15. {1, 1},
  16. {2,3},
  17. }
  18. perim.Distance()

指针接收者的方法

由于主调函数会复制每一个实参变量,如果函数需要更新一个变量,或者如果一个实参太大而我们希望避免复制整个实参,因此我们必须使用指针来传递变量的地址。这也同样适用于更新接受者:我们将它绑定到指针类型,比如*Point, 这被称作指针接收者方法

  1. func (p *Point) ScaleBy(factor float64) {
  2. p.X *= factor
  3. p.Y *= factor
  4. }
  5. 调用方式
  6. r := &Point{1, 2}
  7. r.ScaleBy(2)
  8. fmt.Println(*r)
  9. 或者
  10. p := Point{1, 2}
  11. pptr := &p
  12. pttr.ScaleBy(2)
  13. 或者
  14. p := Point{1, 2}
  15. (&p).ScaleBy(2)

其实直接使用p.ScaleBy(2)也是可以的,因为编译器会对变量进行&p的隐式转化,只能对变量进行操作
如果是字面变量则不行,因为无法获取地址

  1. Point{1, 2}.ScaleBy(2) 不行

反过来一个接收者为*Point类型,以Point的类型去调用也是合法的,因为我们有办法获取Point的值,编译器
会自动插入取值操作如:

  1. pptr.Distance(q)
  2. (*pptr).Distance(q)

在真实的程序中,习惯上遵循如果Point的任何一个方法使用指针接收者,那么所有的Point方法都应该使用指针接收者,即使有些方法不一定需要。 以上的函数名应该是(Point).ScaleBy,如果不加括号,表达式会被解析成 (Point.ScaleBy)。

为了防止混淆,不允许本身是指针的类型进行方法声明,如

  1. type P *int
  2. func (P) f() {} // 非法

关于指针接收者的方法的小节:

只有符合下面三种形式的语句才能成立 (形参是函数参数,实参是实际调用的参数)

  1. 实参接收者和形参接收者都是同一个类型,比如都是T类型或者*T类型
  2. 实参接收者是T类型的变量,而形参接收者是*T类型,编译器会隐式的获取变量的地址
  3. 实参是*T类型,而形参接收者是T类型,编译器会隐式的获取真实的取值

如果所有类型T方法的接收者是T自己,而非(*T), 那么复制它的实例是安全的;调用方法的时候都必须进行一次复制,但是任何方法的接受者是指针的情况下,就应该避免复制T的实例,有很多实例后T对象就会变得不稳定,因为这么做可能破坏原本的数据结构,比如复制bytes.Buffer实例只会得到相当于原来bytes数组的一个别名,随后的方法调用会产生不可预期的结果。

nil是一个合法的接收者

就像一些函数允许nil指针作为实参,方法的接收者也是一样,尤其是nil是类型中有意义的零值

  1. type IntList struct {
  2. Value int
  3. Tail *IntList
  4. }
  5. func (list *IntList) Sum() int {
  6. if list == nil {
  7. return 0
  8. }
  9. return list.Value + list.Tail.Sum()
  10. }

当定义一个类型允许nil作为接收者时,应当在文档注释中显示的标明。

  1. type Values map[string][]string
  2. func (v Values) Get(key string) string {
  3. if vs := v[key]; len(vs) > 0 {
  4. return vs[0]
  5. }
  6. return ""
  7. }
  8. func (v Values) Add(key, value string) {
  9. v[key] = append(v[key], value)
  10. }

Values的实现是map类型,通过定义接收者方法,简化了map操作,它的值是字符串slice,既可以使用map的方法,也可以使用添加的其他方法。

结构体内嵌组成类型

结构体内嵌结构体的方法