go学习记录——第三天

今天也继续学习go吧。今天找了下《The Way to Go》这本书的中译本,书作者是Ivo Balbaert。也非常感谢翻译者们的努力,在这里贴上链接The Way to Go,也希望大家能支持下翻译者们,给项目加个小星星。

根据对比,这个讲的比较完整,咱就准备根据这个进行学习,后续也会找一些其他资料来学习。早知道就直接在GitHub上找资料来学习了🤣,事实证明GitHub才是好东西。

指针

go的指针不同于C的指针,go的指针不能进行算术运算,但是可以通过指针控制特定集合的数据结构、分配的数量及内存的访问模式。

go的取地址符号是&,放在变量前即可获得该变量的地址。

实例

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

import "fmt"

func main() {
var i1 = 5
fmt.Printf("An integer: %d, its location in memory: %p\n", i1, &i1)

var intP *int
intP = &i1
fmt.Printf("The value at memory location %p is %d\n", intP, *intP)
}

这里的第一个输出的便是i1的地址

而在第二个输出中,定义了一个指向int的指针,用*i表示,然后我们用intP就可以调用。而后intP的值为i1的地址,所以输出的结果为5且地址为原始i1的地址。

指针格式化标识符为%p。(关于这个后续写个表格都列一下,好像还蛮多的)

然后捋一下第二个输出,intP -> &i1,所以引用了i1。没有指向任何东西的时候指针的默认值为nil,通常来说指针缩写是prt

再来另外一个例子

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

import "fmt"

func main() {
s := "good bye"
var p *string = &s
*p = "ciao"

fmt.Printf("Here is the pointer p: %p\n", p) // prints address
fmt.Printf("Here is the string *p: %s\n", *p) // prints string
fmt.Printf("Here is the string s: %s\n", s) // prints same string
}
/*output
Here is the pointer p: 0x2540820
Here is the string *p: ciao
Here is the string s: ciao
*/

这里可以看到,当我们在使用指针的时候,第一个输出由于还没有进行操作,所以p指向的是s的地址。而第二个输出*p已经被赋了新值,而后s也被改变了,所以输出都是ciao。

需要注意的是指针不能获取字面量和常量的地址。

指针的一个高级应用是你可以传递一个变量的引用(如函数的参数),这样不会传递变量的拷贝。指针传递是很廉价的,只占用 4 个或 8 个字节。当程序在工作中需要占用大量的内存,或很多变量,或者两者都有,使用指针会减少内存占用和提高效率。被指向的变量也保存在内存中,直到没有任何指针指向它们,所以从它们被创建开始就具有相互独立的生命周期。

另一方面(虽然不太可能),由于一个指针导致的间接引用(一个进程执行了另一个地址),指针的过度频繁使用也会导致性能下降。

指针也可以指向另一个指针,并且可以进行任意深度的嵌套,导致你可以有多级的间接引用,但在大多数情况这会使你的代码结构不清晰。

函数

这里将根据这本书的相关内容对之前学习的内容进行补充。

首先先做一下书中提出的问题

如下的两个函数调用有什么不同:

1
2
3
4
5
6
7
(A) func DoSomething(a *A) {
b = a
}

(B) func DoSomething(a A) {
b = &a
}

首先A中,a指向了指针*A,所以在后续是可以直接对A值本身进行修改,也就是说b现在指向了和a一样的地址,后续函数内对b的修改同样会影响a

B中,a指向的是值A,而b指向的是a的地址(副本),也就是调用了这个值(副本),并不会影响a原本的值

函数返回值

函数返回值可以使用returnreturn var,但这里就有一个问题,返回值是一个表达式的时候可能会因为没有明确的目标而报错(虽然我写python的时候也老干这个,但好像没啥影响)。所以尽可能用简短的,好辨识的返回值来命名,尽可能减少使用表达式。

改变外部变量

这里就需要用到指针了,实例如下

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
}

这里可以看到,我们在主函数部分让reply指向了n的地址,后续我们在Multiply函数中对reply的修改会影响到n,也就是通过*reply修改了外部变量的值。这里我们就是按引用传递而不是按值传递.

传递变长参数

如果函数的最后一个参数是...type的形式,这个函数就可以处理一个变长的参数,这个长度可以为0,故而被称为变参函数。

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

实例

1
2
func Greeting(prefix string, who ...string)
Greeting("hello:", "Joe", "Anna", "Eileen")

Greeting函数中,变量who便是变长参数,值便是[]string{"Joe", "Anna", "Eileen"}

如果参数被存储在一个 slice 类型的变量 slice 中,则可以通过 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}
x = min(slice...)
fmt.Printf("The minimum in the slice 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
}
/*output
The minimum is: 0
The minimum in the slice is: 1
*/

一个接受变长参数的函数可以将这个参数作为其他函数的参数进行传递

1
2
3
4
5
6
7
func F1(s ...string) {
F2(s...)
F3(s)
}

func F2(s ...string) { }
func F3(s []string) { }

变长参数可以作为对应类型的slice进行二次传递

如果变长参数的类型不同,有两种方式可以解决

  1. 使用结构,定义一个结构类型,假设它叫Options,用于存储所有可能的参数
    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})。
  2. 使用空接口,如果一个变长参数的类型没有被指定,则默认为空接口interface{},则就可以接入任何类型的参数。这种情况下我们不仅可以接入任何类型的参数,即使参数长度不固定也是没问题的。
1
2
3
4
5
6
7
8
9
10
11
func typecheck(..,..,values … interface{}) {
for _, value := range values {
switch v := value.(type) {
case int: …
case float: …
case string: …
case bool: …
default: …
}
}
}

defer和追踪

关键词defer允许我们推迟到函数返回之前(或任意位置执行return语句之后)一刻才执行某个语句或函数。而返回的原因就是在return中同样包含一些操作,并不是单纯返回一个值。

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

func main() {
function1()
}

func function1() {
fmt.Printf("In function1 at the top\n")
defer function2()
fmt.Printf("In function1 at the bottom!\n")
}

func function2() {
fmt.Printf("Function2: Deferred until the end of the calling function!\n")
}
/*output
In Function1 at the top
In Function1 at the bottom!
Function2: Deferred until the end of the calling function!
*/

这里我们在执行后可以看到是在function1的第二个Println之后才去执行function2。我们再对比下去掉defer关键词的结果

1
2
3
In function1 at the top
Function2: Deferred until the end of the calling function!
In function1 at the bottom!

这里我们就可以很清楚的看到,加入了defer关键词,我们的function2就被在运行到的时候被挂起,等function1的执行结束后立刻接在后面,这也就是概念中提到的推迟。

使用defer关键词还可以接受参数,下面这个例子就是因为defer直接接受了参数i = 0,然后挂起。即使后面i = 1还是会用挂起时的数进行输出

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

func main() {
i := 0
defer fmt.Println(i)
i++
return
}

当有多个defer行被注册时,将会以逆序进行运行

1
2
3
4
5
6
7
8
func f() {
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i)
}
}
/*output
4 3 2 1 0
*/

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()

下面一个实例来结合上面的第四种操作

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
30
31
32
33
34
35
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
}
/*output
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
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
24
25
26
27
28
29
30
31
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()
}
/*output
entering: b
in b
entering: a
in a
leaving: a
leaving: b
*/

这里leaving先a后b是因为挂起是b先挂起,所以后输出

这里还有一个例子说明另一种defer语句,也就是使用defer语句来记录函数的参数和返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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
2024/08/09 15:17:05 func1("Go") = 7, EOF
*/

内置函数

名称 说明
close() 用于管道通信
len()cap() len() 用于返回某个类型的长度或数量(字符串、数组、切片、map 和管道);cap() 是容量的意思,用于返回某个类型的最大容量(只能用于数组、切片和管道,不能用于 map
new()make() new()make() 均是用于分配内存:new() 用于值类型和用户定义的类型,如自定义结构,make 用于内置引用类型(切片、map 和管道)。它们的用法就像是函数,但是将类型作为参数:new(type)make(type)new(T) 分配类型 T 的零值并返回其地址,也就是指向类型 T 的指针。它也可以被用于基本类型:v := new(int)make(T) 返回类型 T 的初始化之后的值,因此它比 new() 进行更多的工作。new() 是一个函数,不要忘记它的括号。
copy()append() 用于复制和连接切片
panic()recover() 两者均用于错误处理机制
print()println() 底层打印函数,在部署环境中建议使用 fmt
complex()real ()imag() 用于创建和操作复数

递归函数

最经典的就是计算斐波那契数列

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
30
31
32
33
package main

import "fmt"

func main() {
result := 0
for i := 0; i <= 10; i++ {
result = fibonacci(i)
fmt.Printf("fibonacci(%d) is: %d\n", i, result)
}
}

func fibonacci(n int) (res int) {
if n <= 1 {
res = 1
} else {
res = fibonacci(n-1) + fibonacci(n-2)
}
return
}
/*output
fibonacci(0) is: 1
fibonacci(1) is: 1
fibonacci(2) is: 2
fibonacci(3) is: 3
fibonacci(4) is: 5
fibonacci(5) is: 8
fibonacci(6) is: 13
fibonacci(7) is: 21
fibonacci(8) is: 34
fibonacci(9) is: 55
fibonacci(10) is: 89
*/

go还可以函数之间相互递归调用,由于go的特性,函数声明的位置可以任意

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
30
31
32
33
34
35
36
37
38
39
package main

import (
"fmt"
)

func main() {
fmt.Printf("%d is even: is %t\n", 16, even(16)) // 16 is even: is true
fmt.Printf("%d is odd: is %t\n", 17, odd(17))
// 17 is odd: is true
fmt.Printf("%d is odd: is %t\n", 18, odd(18))
// 18 is odd: is false
}

func even(nr int) bool {
if nr == 0 {
return true
}
return odd(RevSign(nr) - 1)
}

func odd(nr int) bool {
if nr == 0 {
return false
}
return even(RevSign(nr) - 1)
}

func RevSign(nr int) int {
if nr < 0 {
return -nr
}
return nr
}
/*
16 is even: is true
17 is odd: is true
18 is odd: is false
*/

闭包的补充

实例中将会看到函数Add2()Adder()均返回签名为func(b_int) int的函数

Add2()不接受任何参数,而Adder()接受一个int类型的参数

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

func Add2() func(b int) int {
return func(b int) int {
return b + 2
}
}

func Adder(a int) func(b int) int {
return func(b int) int {
return a + b
}
}

下面这个实例是另外一种写法实现

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

import "fmt"

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
return func(delta int) int {
x += delta
return x
}
}

使用闭包调试

在分析和调试复杂程序时,无数个函数在代码之间相互调用,如果能准确知道哪个文件中哪个函数在运行会对分析有很大的帮助。可以使用 runtimelog 包中的特殊函数来实现这样的功能。包 runtime 中的函数 Caller() 提供了相应的信息,因此可以在需要的时候实现一个 where() 闭包函数来打印函数执行的位置:

1
2
3
4
5
6
7
8
9
where := func() {
_, file, line, _ := runtime.Caller(1)
log.Printf("%s:%d", file, line)
}
where()
// some code
where()
// some more code
where()

也可以设置log包中的flag参数

1
2
log.SetFlags(log.Llongfile)
log.Print("")

或使用更精简版的where()函数

1
2
3
4
5
6
7
8
var where = log.Print
func func1() {
where()
... some code
where()
... some code
where()
}

这部分还是等后面再深入学吧🤣,咱连很多基础内容都没学呢。

得嘞,今儿就到这吧,bye~


go学习记录——第三天
https://www.lx02918.ltd/2024/08/09/go-study-third-day/
作者
Seth
发布于
2024年8月9日
许可协议