找回密码
 立即注册
搜索
查看: 273|回复: 0

[go基础] 《Go语言入门指南》学习笔记 - 函数 Function

  [复制链接]
匿名
匿名  发表于 2023-2-8 15:57 |阅读模式

0x01 基本介绍

return可以结束for死循环或者结束一个协程(goroutine)

Go 里面有三种类型的函数:

  • 普通的带有名字的函数

  • 匿名函数或者lambda函数

  • 方法(Method)

函数参数、返回值以及它们的类型被统称为函数签名。

0x02 函数参数与返回值

1. 函数传参

  • 按值传递(call by value):传递参数的副本,Function(arg1)
  • 按引用传递(call by reference):传递参数的地址,Function(&arg1)

切片(slice)、字典(map)、接口(interface)、通道(channel)默认使用引用传递,即使没有显示的指出指针

2. 命名的返回值 named return variables

什么是命名的返回值,直接上示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 非命名的返回值
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
}

尽量使用命名返回值:会使代码更清晰、更简短,同时更加容易读懂。

函数返回值处理方式:

  • 1-3 个:直接返回
  • 4-5 个,且返回值类型相同:返回切片
  • 大于6个,且返回值类型不同:返回结构体

3. 空白符 blank identifier

空白符_ ,用来匹配一些不需要的值,然后丢弃掉。

1
2
i1, _, f1 = ThreeValues()
// 函数ThreeValues(),有三个返回值,第一个给i1,第三个给f1,第二个丢弃掉

4. 改变外部变量 outside variable

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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
}

0x03 传递变长参数

变长参数 ,函数的最后一个参数是采用 ...type 的形式。

变长参数,长度可以是0个。

基本用法:

1
2
3
func myFunc(a, b, arg ...int) {
  
}

传参类型相同用slice,类型不同用结构体

1. 使用 slice

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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}
    // 传递变长参数
    // 通过 slice... 的形式来传递参数调用变参函数
    x = min(slice...)
    fmt.Printf("The minimum in the slice is: %d", x)
}

// 变长参数 s
func min(s ...int) int {
    if len(s)==0 {
        return 0
    }
    min := s[0]
    // 使用for-range结构对变参进行迭代
    for _, v := range s {
        if v < min {
            min = v
        }
    }
    return min
}

2. 使用结构体

先定义一个结构体

1
2
3
4
5
type Options struct {
    par1 type1,
    par2 type2,
    ...
}

函数 F1 可以使用正常的参数 a 和 b,以及一个没有任何初始化的 Options 结构: F1(a, b, Options {})

如果需要对选项进行初始化,则可以使用 F1(a, b, Options {par1:val1, par2:val2})

3. 使用空接口

如果一个变长参数的类型没有被指定,则可以使用默认的空接口 interface{},这样就可以接受任何类型的参数。

适用情况:参数长度未知,类型未知

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func typecheck(..,..,values  interface{}) {
    for _, value := range values {
      	// 类型断言
        switch v := value.(type) {
            case int: 
            case float: 
            case string: 
            case bool: 
            default: 
        }
    }
}

0x04 defer 和 追踪

defer 用法:类似面向对象编程语言中的finally语块。

defer 执行时机:

  • 函数返回之前
  • 任意位置执行 return 语句之后

为什么要在返回之后才执行这些语句?

因为 return 语句同样可以包含一些操作,而不是单纯地返回某个值

defer 作用:

  • 推迟语句或函数执行

defer 用途:

一般用于释放某些已分配的资源,例如:

  1. 关闭文件流

    1
    2
    
    // open a file  
    defer file.Close()
    
  2. 解锁一个加锁的资源

    1
    2
    
    mu.Lock()  
    defer mu.Unlock() 
    
  3. 打印最终报告

    1
    2
    
    printHeader()  
    defer printFooter()
    
  4. 关闭数据库链接

    1
    2
    
    // open a database connection  
    defer disconnectFromDB()
    

上述4种方式示例代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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
}

执行结果:

ok, connected to db
Defering the database disconnect.
Doing some DB operations ...
Oops! some crash or network error ...
Returning from function here!
ok, disconnected from db

使用 defer 语句实现代码追踪

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

1
2
3
4
5
6
func trace(s string) { 
  fmt.Println("entering:", s) 
}
func untrace(s string) { 
  fmt.Println("leaving:", s) 
}

示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

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

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

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

func main() {
    b()
}

输出结果:

entering: b
in b
entering: a
in a
leaving: a
leaving: b

更加简便的版本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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()
}

使用 defer 语句来记录函数的参数与返回值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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

0x05 内置函数

0x06 递归函数

0x07 将函数作为参数

函数可以作为其它函数的参数进行传递,然后在其它函数内调用执行,一般称之为回调。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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()

0x08 闭包

不想给函数起名字的函数,叫匿名函数。

匿名函数同样被称之为闭包(函数式语言的术语):它们被允许调用定义在其它环境下的变量。闭包可使得某个函数捕捉到一些外部状态,例如:函数被创建时的状态。

另一种表示方式为:一个闭包继承了函数所声明时的作用域。这种状态(作用域内的变量)都被共享到闭包的环境中,因此这些变量可以在闭包中被操作,直到被销毁。

闭包经常被用作包装函数:它们会预先定义好 1 个或多个参数以用于包装。

另一个不错的应用就是使用闭包来完成更加简洁的错误检查。

1. 匿名函数赋值给变量

例如

1
2
3
4
// 匿名函数没有名字
func(x, y int) int { 
	return x + y 
}

这样的一个函数不能够独立存在,编译器会返回错误:

non-declaration statement outside function body

所以必须把函数赋给一个变量,即变量保存函数的地址

1
2
3
4
5
6
// 把匿名函数赋给变量 fplus,变量 fplus 保存函数的地址
fplus := func(x, y int) int {
	return x + y 
}
// 然后,通过变量名对函数进行调用:
fplus(3,4)

2. 另一种写法,直接对匿名函数进行调用

1
2
3
4
5
6
7
func() {
    sum := 0
    for i := 1; i <= 1e6; i++ {
        sum += i
    }
}() 
// 最后的一对括号表示对该匿名函数的调用

3. 匿名函数传参

1
2
3
4
5
6
// 直接调用方式使用匿名函数
// 传参 v 给 匿名函数变量 u
func (u string) {
    fmt.Println(u)
    
}(v)

4. defer 语句和匿名函数

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

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

5. 关键字 go 和 匿名函数

匿名函数配合关键字go作为 goroutine 使用,开启一个协程。

1
2
3
go func(){
	...
}

0x09 应用闭包:将函数作为返回值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import "fmt"

func main() {
    // make an Add2 function, give it a name p2, and call it:
    p2 := Add2()
    fmt.Printf("Call Add2 for 3 gives: %v\n", p2(3))
    // make a special Adder function, a gets value 2:
    TwoAdder := Adder(2)
    fmt.Printf("The result is: %v\n", TwoAdder(3))
}


// Add2 函数不传入参数
func Add2() func(b int) int {
    // 闭包
    return func(b int) int {
        return b + 2
    }
}

// Adder 函数 传入一个参数 a
func Adder(a int) func(b int) int {
    // 闭包
    return func(b int) int {
        return a + b
    }
}

输出结果:

Call Add2 for 3 gives: 5
The result is: 5

闭包函数保存并积累其中的变量的值,不管外部函数退出与否,它都能够继续操作外部函数中的局部变量。

请看下面示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

// 三次调用函数 f 的过程中函数 Adder () 中变量 delta 的值分别为:1、20 和 300。
// 在多次调用中,变量 x 的值是被保留的,即 0 + 1 = 1,然后 1 + 20 = 21,最后 21 + 300 = 321
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 // 声明变量 x
    return func(delta int) int {
        x += delta // 闭包函数中对变量 x 进行赋值
        return x
    }
}

输出结果:

1 - 21 - 321

在闭包中使用到的变量可以是在闭包函数体内声明的,也可以是在外部函数声明的:

1
2
3
4
5
6
7
8
// 闭包函数外声明变量 g 
var g int
go func(i int) {
    s := 0
    for j := 0; j < i; j++ { s += j }
    // 闭包函数内为变量 g 赋值,这样就可以修改全局变量 g
    g = s
}(1000) // Passes argument 1000 to the function literal.

这样闭包函数就能够被应用到整个集合的元素上,并修改它们的值。

然后这些变量就可以用于表示或计算全局或平均值。

一个返回值为另一个函数的函数可以被称之为工厂函数。

在需要创建一系列相似的函数的时候非常有用:书写一个工厂函数而不是针对每种情况都书写一个函数。

下面的函数演示了如何动态返回追加后缀的函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 工厂函数 MakeAddSuffix(suffix string)
// 函数 MakeAddSuffix(suffix string) 返回值 为另一个函数(通过闭包实现返回)
func MakeAddSuffix(suffix string) func(string) string {
    return func(name string) string {
        if !strings.HasSuffix(name, suffix) {
            return name + suffix
        }
        return name
    }
}

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

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

然后调用工厂函数产生的函数:

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

可以返回其它函数的函数和接受其它函数作为参数的函数均被称之为高阶函数,是函数式语言的特点

0x10 使用闭包调试

想要知道哪个文件哪个函数正在执行

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

import (
	"log"
	"runtime"
)

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

输出结果:

1
2
➜  chapter_6 go run bibao_debug.go 
2020/07/05 10:41:32 /Users/luozhibo/Downloads/the-way-to-go_ZH_CN-master/eBook/examples/chapter_6/bibao_debug.go:13

也可以使用log.setFlags(log.Llongfile)设置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

import (
	"log"
)

func main() {
	// where := func() {
	// 	_, file, line, _ := runtime.Caller(1)
	// 	log.Printf("%s:%d", file, line)
	// }
	// where()
	log.SetFlags(log.Llongfile)
	log.Print("我在哪里执行")
}

输出结果:

1
2
➜  chapter_6 go run bibao_debug.go
/Users/luozhibo/Downloads/the-way-to-go_ZH_CN-master/eBook/examples/chapter_6/bibao_debug.go:14: 我在哪里执行

0x11 计算函数的执行时间

1
2
3
4
5
start := time.Now() // 开始
longCalculation()
end := time.Now() // 结束
delta := end.Sub(start) // 相减
fmt.Printf("longCalculation took this amount of time: %s\n", delta)

 

原文地址:http://www.manoner.com/post/GoLand/Go%E8%AF%AD%E8%A8%80%E5%85%A5%E9%97%A8%E6%8C%87%E5%8D%97%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0-%E5%87%BD%E6%95%B0/

 

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|手机版|小黑屋|学习笔记

GMT+8, 2024-12-22 09:17 , Processed in 0.034125 second(s), 13 queries , APCu On.

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表