【读书笔记&个人心得】第6章:函数
概览
Go 是编译型语言,所以函数编写的顺序是无关紧要的;鉴于可读性的需求,最好把 main() 函数写在文件的前面,其他函数按照一定逻辑顺序进行编写(例如函数被调用的顺序)
JS 是解释型语言,因此要注意先定义 function 再调用
DRY
事实上,好的程序是非常注意 DRY 原则的,即不要重复你自己 (Don't Repeat Yourself),意思是执行特定任务的代码只能在程序里面出现一次。
Go 函数类型
Go 里面有三种类型的函数
普通的带有名字的函数
匿名函数或者 lambda 函数(参考 第 6.8 节)
方法(Methods,参考 第 10.6 节)
main()、init()
main()、init()无参数和返回值
理论上,函数调用其他函数的次数是无穷的(直到函数调用栈被耗尽)
函数调用其他函数
在 Go 里面函数重载是不被允许的,使用函数调用其他函数代替。Go 语言不支持这项特性的主要原因是函数重载需要进行多余的类型匹配影响性能。所以你需要给不同的函数使用不同的名字,我们通常会根据函数的特征对函数进行命名(参考 第 11.12.5 节)
函数可以将其他函数调用作为它的参数,只要这个被调用函数的返回值个数、返回值类型和返回值的顺序与调用函数所需求的实参是一致的。
假设 f1 需要 3 个参数 f1(a, b, c int),同时 f2 返回 3 个参数 f2(a, b int) (int, int, int),就可以这样调用
f1:f1(f2(a, b))
声明在外部定义的函数
你只需要给出函数名与函数签名,不需要给出函数体
func flushICache(begin, end uintptr) // implemented externally
使用 type 声明函数类型
type binOp func(int, int) int
把函数名赋值给变量
func bunOp(a int, b int) int {
return a + b
}
add := binOp
这个变量知道自己指向的函数的签名,所以给它赋一个具有不同签名的函数值是不可能的。
函数值比较
函数值 (functions value) 之间可以相互比较:如果它们引用的是相同的函数或者都是 nil 的话,则认为它们是相同的函数
函数嵌套声明
函数不能在其它函数里面声明(不能嵌套),不过我们可以通过使用匿名函数(参考 第 6.8 节)来破除这个限制。
泛型
目前 Go 没有泛型 (generic) 的概念,也就是说它不支持那种支持多种类型的函数(一个参数支持多种类型),不过在大部分情况下可以通过接口 (interface),特别是空接口与类型选择(type switch,参考 第 11.12 节)与/或者通过使用反射(reflection,参考 第 6.8 节)来实现相似的功能。使用这些技术将导致代码更为复杂、性能更为低下,所以在非常注意性能的的场合
最好是为每一个类型单独创建一个函数,而且代码可读性更强。
目前已经支持泛型:https://blog.csdn.net/RA681t58CJxsgCkJ31/article/details/123860904
函数参数与返回值
任何一个有返回值(单个或多个)的函数都必须以 return 或 panic(参考 第 13 章)结尾(函数可以没有返回值)
如果一个函数需要返回值,那么这个函数里面的每一个代码分支 (code-path) 都要有 return 语句
我们也可以定义没有形参名的函数,只有相应的形参类型
func f(int, int, float64)
按值传递 (call by value) 按引用传递 (call by reference)
默认使用按值传递来传递参数
如果你希望函数可以直接修改参数的值,而不是对参数的副本进行操作,你需要将参数的地址(变量名前面添加 & 符号,比如 &variable)传递给函数,这就是按引用传递,比如 Function(&arg1),此时传递给函数的是一个指针
【本质】指针也是变量类型,有自己的地址和值,通常指针的值指向一个变量的地址。所以,按引用传递也是按值传递
在函数调用时,像切片 (slice)、字典 (map)、接口 (interface)、通道 (channel) 这样的引用类型都是默认使用引用传递(即使没有显式的指出指针)
传指针的好处
几乎在任何情况下,传递指针(一个 32 位或者 64 位的值)的消耗都比传递副本来得少(因为指针最大占用 64 位 bit,即 8 个字节,而传副本总是复制复制复制的,积少成多)
64 位平台下,Go 指针自身的大小为什么是 8 字节?
纠偏:
32 位操作系统针对的 32 位的 CPU 设计。CPU 内部寄存器和寻址总线是 32 位,指令集可以运行 32 位数据指令,根据寻址空间的介绍,CPU 的寻址能力以字节为单位 ,则 32 位寻址的 CPU 可以寻址 2 的 32 次方大小的地址也就是 4GB, 而不是 4Gb,此处 32 位不可以理解为 32bit(4 个字节,32bit=4Byte)。32 位 CPU 有 4G 的内存寻址空间。
指针变量即一个值的内存地址,它所指向的值的内存地址在 32 和 64 位机器上分别占用 4 或 8 个字节,占用字节的大小与所指向的值的大小无关。当一个指针被定义后没有分配到任何变量时,它的默认值为 nil。
64 位平台下,字长是 64 位(CPU 一次能处理二进制数据的位数),每一个位可以是 0 或 1,产生 2^64 种变化,若一种变化表示某个数,则可以表示 2^64 个数,因此,64 位 CPU 的地址总线可寻址范围 为 0 ~ 2^64-1,此时注意,CPU 按字节寻址,因此字节编号便是 0 ~ 2^64-1,一个字节是一个房间,房间内 8bit 用于存储值,而有时候一个数据由好几个字节组成,往往是连续使用 2~3 个字节,我们的指针就存开头第一个字节即可(8 个字节表示的范围==2^64)
参考资料:
https://jishuin.proginn.com/p/763bfbd3a588.
https://baike.baidu.com/item/32位/5812218?fr=aladdin
谭浩强 C 语言教程:指针是什么
副作用 (side-effect)
有些函数只是完成一个任务,并没有返回值。我们仅仅是利用了这种函数的副作用 (side-effect),就像输出文本到终端,发送一个邮件或者是记录一个错误等。
很多返回值
如果一个函数需要返回四到五个值,我们可以传递一个切片给函数(如果返回值具有相同类型)或者是传递一个结构体(如果返回值具有不同的类型)。因为传递一个指针允许直接修改变量的值,消耗也更少
// You can edit this code!
// Click here and start typing.
package main
import "fmt"
type Book struct {
title string
author string
price int
}
var Numbers = make([]int, 5, 5)// make([]T, length, capacity) length是初始化时填充0值的长度,length<=capacity
func main() {
fmt.Println(lotsOfReturn())
fmt.Println(lotsOfReturn2())
}
func lotsOfReturn() Book {
return Book{"The Way to Go", "people", 98}
}
func lotsOfReturn2() []int {
return Numbers
}
命名的返回值
也就是说,在声明返回值类型时,把承载返回值的变量名称也指明,就像下面的(x2 int, x3 int)
尽量使用命名返回值:会使代码更清晰、更简短,同时更加容易读懂
package main
import "fmt"
var num int = 10
var numx2, numx3 int
func main() {
numx2, numx3 = getX2AndX3(num)
PrintValues()
numx2, numx3 = getX2AndX3_2(num)
PrintValues()
}
func PrintValues() {
fmt.Printf("num = %d, 2x num = %d, 3x num = %d\n", num, numx2, numx3)
}
func getX2AndX3(input int) (int, int) {
return 2 * input, 3 * input
}
func getX2AndX3_2(input int) (x2 int, x3 int) { // x2 x3在函数调用时就已经被赋予了一个初始零值
x2 = 2 * input
x3 = 3 * input
// return x2, x3
return
}
编写一个名字为 MySqrt() 的函数,计算一个 float64 类型浮点数的平方根,如果参数是一个负数的话将返回一个错误。编写两个版本,一个是非命名返回值,一个是命名返回值。
package main
import (
"fmt"
"math"
)
func main() {
fmt.Println(MySqrt(0.1))
fmt.Println(MySqrt2(0.1))
}
func MySqrt(num float64) float64 {
sqrtNum := math.Sqrt(num)
return sqrtNum
}
func MySqrt2(num float64) (sqrtNum float64) {
sqrtNum = math.Sqrt(num)
return
}
空白符
空白符用来匹配一些不需要的值,然后丢弃掉
package main
import "fmt"
func main() {
var min, max, differ int
min, _, max = MinMax(78, 65)
_, differ, _ = MinMax(78, 65)
fmt.Printf("Minmium is: %d, Maximum is: %d, Differ is %d\n", min, max, differ)
}
func MinMax(a int, b int) (min int, differ int, max int) {
if a < b {
min = a
max = b
} else { // a = b or a < b
min = b
max = a
}
differ = max - min
return
}
改变外部变量
传递指针给函数不但可以节省内存(因为没有复制变量的值),而且赋予了函数直接修改外部变量的能力,所以被修改的变量不再需要使用 return 返回
传递一个指针很容易引发一些不确定的事,所以,我们要十分小心那些可以改变外部变量的函数
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
}
传递变长参数
最后一个参数是采用 ...type 的形式
这个函数接受一个类似于切片 (slice) 的参数(详见第 7 章),该参数可以通过第 5.4.4 节 中提到的 for 循环结构迭代。
如果参数被存储在一个 slice 类型的变量 slice 中,则可以通过 slice... 的形式来传递参数,调用变参函数
写一个函数,该函数接受一个变长参数并对每个元素进行换行打印。
package main
import "fmt"
func main() {
strs := []string{"你好", "张三", "我是", "李四"}
show("你好张三我是李四")
show(strs...)
}
func show(s ...string) {
for i, v := range s {
fmt.Println(i, v)
}
}
输出
0 你好张三我是李四
0 你好
1 张三
2 我是
3 李四
一个接受变长参数的函数可以将这个参数作为其它函数的参数(再次作为变长参数或作为切片)进行传递:
func F1(s ...string) {
F2(s...)
F3(s)
}
func F2(s ...string) { }
func F3(s []string) { }
变长参数的类型不都相同
1.使用结构体(比较复杂,见第10章)
2.使用空接口
如果一个变长参数的类型没有被指定,则可以使用默认的空接口 interface{},这样就可以接受任何类型的参数(详见第 11.9 节 )。该方案不仅可以用于长度未知的参数,还可以用于任何不确定类型的参数。一般而言我们会使用一个 for-range 循环以及 switch 结构对每个参数的类型进行判断:
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 语句同样可以包含一些操作,而不是单纯地返回某个值)
小试牛刀
package main
import "fmt"
func main() {
function1()
}
func function1() {
fmt.Println(1)
defer function2()
fmt.Println(3)
}
func function2() {
fmt.Println(2)
}
输出:
1
3
2
使用 defer 的语句同样可以接受参数
func a() {
i := 0
defer fmt.Println(i)
i++
return
}
多个 defer
当有多个 defer 行为被注册时,它们会以逆序执行(类似栈,即后进先出):
func f() {
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i)
}
}
deffer应用
关闭文件流 (详见 第 12.2 节)
// open a file
defer file.Close()
解锁一个加锁的资源 (详见 第 9.3 节)
mu.Lock()
defer mu.Unlock()
打印最终报告
printHeader()
defer printFooter()
关闭数据库链接
// open a database connection
defer disconnectFromDB()
使用 defer 语句实现代码追踪
就行JS调试时的debugger一样,用defer可以观察一个函数是否被执行完毕,以及多个函数的执行顺序(我觉得不需要打印开始执行,因为你不开始执行,根本不会执行defer)
package main
import "fmt"
func a() {
defer fmt.Printf("执行完毕a")
}
func b() {
defer fmt.Printf("执行完毕b")
}
func main() {
a()
b()
}
另一种方式
使用 defer 语句来记录函数的参数与返回值
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")
}
内置函数
Go 语言拥有一些不需要进行导入操作就可以使用的内置函数
close()、new()、make()、copy()、append()、panic()、recover()、print()、println()(建议使用 fmt 包的)、complex()、real ()、imag()、len()、cap()
递归函数
当一个函数在其函数体内调用自身,则称之为递归
例子:计算斐波那契数列,即前两个数为 1,从第三个数开始每个数均为前两个数之和。
package main
import "fmt"
func main() {
for i := 1; i <= 10; i++ {
fmt.Printf("%d\n", fibonacci(i))
}
}
func fibonacci(num int) (res int) {
if num <= 2 {
res = 1
} else {
res = fibonacci(num-2) + fibonacci(num-1)
}
return
}
互相调用(循环调用)
Go 语言中也可以使用相互调用的递归函数:多个函数之间相互调用形成闭环。因为 Go 语言编译器的特殊性,这些函数的声明顺序可以是任意的。
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
}
重写本节中生成斐波那契数列的程序并返回两个命名返回值(详见第 6.2 节),即数列中的位置和对应的值,例如 5 与 4,89 与 10
package main
import "fmt"
func main() {
for i := 1; i <= 10; i++ {
index, res := fibonacci(i)
fmt.Printf("%d %d\n", index, res)
}
}
func fibonacci(num int) (index int, res int) {
if num <= 2 {
res = 1
} else {
_, f1 := fibonacci(num - 2)
_, f2 := fibonacci(num - 1)
res = f1 + f2
}
index = num
return
}
使用递归函数从 10 打印到 1。
package main
import "fmt"
func main() {
calculateNum(10)
}
func calculateNum(num int) {
if num >= 1 {
fmt.Printf("%d\n", num)
calculateNum(num - 1)
}
}
实现一个输出前 30 个整数的阶乘的程序。
n 的阶乘定义为:n! = n * (n-1)!, 0! = 1,因此它非常适合使用递归函数来实现。
特别注意的是,使用 int 类型最多只能计算到 12 的阶乘,因为一般情况下 int 类型的大小为 32 位,继续计算会导致溢出错误。那么,如何才能解决这个问题呢?
最好的解决方案就是使用 big 包(详见第 9.4 节)
package main
import (
"fmt"
"math/big"
)
func main(){
var num int64
fmt.Scanf("%d",&num)
f:=factorial(num)
fmt.Printf("数字%d的阶乘是%v",num,f)
}
func factorial(num int64) (res *big.Int){
ip := big.NewInt(1)
if num==1 {
return ip
}else {
in1 := big.NewInt(num)
in2 := factorial(num-1)
res = ip.Mul(in1,in2)
}
return
}
将函数作为参数
一般用于在其它函数内调用执行,即回调
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)
}
strings.IndexFunc()
按照一定要求返回该字符的索引值,由于要求是用户自定义的,所以叫做Func,表示抽象一层
例如 strings.IndexFunc(line, unicode.IsSpace) 就会返回 line 中第一个空白字符的索引值(IsSpace是判断是否空白符的func)
func IsAscii(c int) bool {
if c > 255 {
return false
}
return true
}
TODO:ASCII 字符替换成问号 '?' 或空格 ' '
【练习】包 strings 中的 Map() 函数和 strings.IndexFunc() 一样都是非常好的使用例子。请学习它的源代码并基于该函数书写一个程序,要求将指定文本内的所有非 ASCII 字符替换成问号 '?' 或空格 ' '。您需要怎么做才能删除这些字符呢
// strings.Map
func Map(mapping func(rune) rune, s string) string {
// In the worst case, the string can grow when mapped, making
// things unpleasant. But it's so rare we barge in assuming it's
// fine. It could also shrink but that falls out naturally.
// The output buffer b is initialized on demand, the first
// time a character differs.
var b Builder
for i, c := range s {
r := mapping(c)
if r == c && c != utf8.RuneError {
continue
}
var width int
if c == utf8.RuneError {
c, width = utf8.DecodeRuneInString(s[i:])
if width != 1 && r == c {
continue
}
} else {
width = utf8.RuneLen(c)
}
b.Grow(len(s) + utf8.UTFMax)
b.WriteString(s[:i])
if r >= 0 {
b.WriteRune(r)
}
s = s[i+width:]
break
}
// Fast path for unchanged input
if b.Cap() == 0 { // didn't call b.Grow above
return s
}
for _, c := range s {
r := mapping(c)
if r >= 0 {
// common case
// Due to inlining, it is more performant to determine if WriteByte should be
// invoked rather than always call WriteRune
if r < utf8.RuneSelf {
b.WriteByte(byte(r))
} else {
// r is not a ASCII rune.
b.WriteRune(r)
}
}
}
return b.String()
}
参考资料https://www.sdk.cn/details/emdvzb3PQRam8jqQPA
https://www.geeksforgeeks.org/strings-map-function-in-golang-with-examples/
闭包
匿名函数不可以独立存在,但是可以复制给变量
func(x, y int) int { return x + y } //error:non-declaration statement outside function body
fplus := func(x, y int) int { return x + y }
fplus(3,4)
立即调用的匿名函数
func(x, y int) int { return x + y } (3, 4)
func() {
sum := 0
for i := 1; i <= 1e6; i++ {
sum += i
}
}()
匿名函数可以被赋值给变量并作为值使用,变量存的是一个内存地址
package main
import "fmt"
func main() {
f()
}
func f() {
for i := 0; i < 4; i++ {
g := func(i int) { fmt.Printf("%d ", i) }
g(i)
fmt.Printf(" - g is of type %T and has value %v\n", g, g)
}
}
匿名函数和defer
关键字 defer (详见第 6.4 节)经常配合匿名函数使用,它可以用于改变函数的命名返回值
package main
import "fmt"
func f() (ret int) {
return 1
}
func main() {
fmt.Println(f())
}
输出1
package main
import "fmt"
func f() (ret int) {
defer func() {
ret++
}()
return 1
}
func main() {
fmt.Println(f())
}
输出2
变量 ret 的值为 2,因为 ret++ 是在执行 return 1 语句后发生的,这可用于在返回语句之后修改返回的 error 时使用
应用闭包
匿名函数还可以配合 go 关键字来作为 goroutine 使用(详见第 14 章和第 16.9 节)
匿名函数同样被称之为闭包(函数式语言的术语):它们被允许调用定义在其它环境下的变量。闭包可使得某个函数捕捉到一些外部状态,例如:函数被创建时的状态。另一种表示方式为:一个闭包继承了函数所声明时的作用域。这种状态(作用域内的变量)都被共享到闭包的环境中,因此这些变量可以在闭包中被操作,直到被销毁,详见第 6.9 节 中的示例。闭包经常被用作包装函数:它们会预先定义好 1 个或多个参数以用于包装,详见下一节中的示例。另一个不错的应用就是使用闭包来完成更加简洁的错误检查(详见第 16.10.2 节)。(也即是说,闭包一般由匿名函数来构成,因为闭包主要是要取其结构来用,所以函数名叫什么不重要,重要的是形成层次结构)
将函数作为返回值
闭包函数保存并积累其中的变量的值,不管外部函数退出与否,它都能够继续操作外部函数中的局部变量
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
}
}
输出:
1 - 21 - 321
使用外部函数声明的变量
在闭包中使用到的变量可以是在闭包函数体内声明的,也可以是在外部函数声明的
var g int
go func(i int) {
s := 0
for j := 0; j < i; j++ { s += j }
g = s
}(1000) // Passes argument 1000 to the function literal.
由于你可以在使用 import 导入包之后定义或声明 0 个或多个常量 (const)、变量 (var) 和类型 (type),这些对象的作用域都是全局的(在本包范围内),这样又由于闭包能访问外面的值,因此,闭包函数就能够被应用到整个集合的元素上,并修改它们的值。
闭包会不会导致外部变量的变化的停滞?
不会
package main
import "fmt"
var g int
func main() {
var f = a()
fmt.Println(g)
fmt.Println(f())
g++
fmt.Println(f())
}
func a() func() int {
return func() int {
g++
return g
}
}
工厂函数
一个返回值为另一个函数的函数可以被称之为工厂函数,这在您需要创建一系列相似的函数的时候非常有用:书写一个工厂函数而不是针对每种情况都书写一个函数
动态返回追加后缀的函数
package main
import (
"fmt"
"strings"
)
var g int
func main() {
var addDocx = MakeAddSuffix(".docx")
fmt.Println(f("my"))
}
func MakeAddSuffix(suffix string) func(string) string {
return func(name string) string {
if !strings.HasSuffix(name, suffix) {
return name + suffix
}
return name
}
}
可以返回其它函数的函数和接受其它函数作为参数的函数均被称之为高阶函数,是函数式语言的特点
应用闭包2
使用闭包调试
当您在分析和调试复杂的程序时,无数个函数在不同的代码文件中相互调用,如果这时候能够准确地知道哪个文件中的具体哪个函数正在执行,对于调试是十分有帮助的。
package main
import (
"log"
"runtime"
)
var g int
func main() {
where := func() {
_, file, line, _ := runtime.Caller(1)
log.Printf("%s:%d", file, line)// 源文件名称和第几行代码
}
where()
// some code
where()
// some more code
where()
}
我的实验:
runtime.Caller(0) Caller在匿名函数的位置
runtime.Caller(1) (含有Caller的)匿名函数的在其他函数的位置
您也可以设置 log 包中的 flag 参数来实现
package main
import (
"log"
)
var g int
func main() {
log.SetFlags(log.Llongfile)
log.Print("调用数据库") // /tmp/sandbox953969418/prog.go:11: 调用数据库
}
补充:Go Log包使用
其实就是一个日志打印的包。log包定义了Logger类型,该类型提供了一些格式化输出的方法
package main
import (
"log"
)
func main() {
log.Println("这是一条很普通的日志。")
v := "很普通的"
log.Printf("这是一条%s日志。\n", v)
log.Fatalln("这是一条会触发fatal的日志。")
log.Panicln("这是一条会触发panic的日志。")
}
输出:
2021/03/03 14:38:33 这是一条很普通的日志。
2021/03/03 14:38:33 这是一条很普通的日志。
2021/03/03 14:38:33 这是一条会触发fatal的日志。
Process finished with exit code 1
参考资料:https://developer.aliyun.com/article/937832
默认情况下的logger只会提供日志的时间信息
package main
import (
"log"
)
var g int
func main() {
log.Print("")// 2009/11/10 23:00:00
}
如果还想记录该日志的文件名和行号,可以进行配置
log标准库中的Flags函数会返回 标准logger的输出配置,而SetFlags函数用来设置标准logger的输出配置(说白了,就是预设一些配置,你自己组装一下来用)。
可以用的配置如下
const (
// 控制输出日志信息的细节,不能控制输出的顺序和格式。
// 输出的日志在每一项后会有一个冒号分隔:例如2009/01/23 01:23:23.123123 /a/b/c/d.go:23: message
Ldate = 1 << iota // 日期:2009/01/23
Ltime // 时间:01:23:23
Lmicroseconds // 微秒级别的时间:01:23:23.123123(用于增强Ltime位)
Llongfile // 文件全路径名+行号: /a/b/c/d.go:23
Lshortfile // 文件名+行号:d.go:23(会覆盖掉Llongfile)
LUTC // 使用UTC时间
LstdFlags = Ldate | Ltime // 标准logger的初始值
)
除此外还可以配置输出位置,如输出到某个文件xx.log中,默认是标准错误输出。
还可以创建属于自己的logger
还可以配置日志前缀,如[小王子]2017/06/19 14:05:57
计算函数执行时间
有时候,能够知道一个计算执行消耗的时间是非常有意义的,尤其是在对比和基准测试中。最简单的一个办法就是在计算开始之前设置一个起始时间,再记录计算结束时的结束时间,最后计算它们的差值,就是这个计算所消耗的时间
start := time.Now()
longCalculation()
end := time.Now()
delta := end.Sub(start)
fmt.Printf("longCalculation took this amount of time: %s\n", delta)
通过内存缓存来提升性能
当在进行大量的计算时,提升性能最直接有效的一种方式就是避免重复计算。通过在内存中缓存和重复利用相同计算的结果,称之为内存缓存(相同计算只算一次)
最明显的例子就是生成斐波那契数列的程序
要计算数列中第 n 个数字,需要先得到之前两个数的值,但很明显绝大多数情况下前两个数的值都是已经计算过的。即每个更后面的数都是基于之前计算结果的重复计算(计算5的阶乘,包含了求4的阶乘,但是前面4的阶乘已经求过了,最好的办法就是能够引用之前的,所谓前面就是以前调函数时)
而我们要做就是将第 n 个数的值存在数组中索引为 n 的位置(详见第 7 章),然后在数组中查找是否已经计算过,如果没有找到,则再进行计算。
优化版斐波那契数列实现:
// fibonacci_memoization.go
package main
import (
"fmt"
"time"
)
const LIM = 41
var fibs [LIM]uint64 // 有点充当哈希表的味道
func main() {
var result uint64 = 0
start := time.Now()
for i := 0; i < LIM; i++ {
result = fibonacci(i)
fmt.Printf("fibonacci(%d) is: %d\n", i, result)
}
end := time.Now()
delta := end.Sub(start)
fmt.Printf("longCalculation took this amount of time: %s\n", delta)
}
func fibonacci(n int) (res uint64) {
// memoization: check if fibonacci(n) is already known in array:
if fibs[n] != 0 {
res = fibs[n]
return
}
if n <= 1 {
res = 1
} else {
res = fibonacci(n-1) + fibonacci(n-2)
}
fibs[n] = res
return
}
/*
Output: LIM=40:
normal (fibonacci.go): the calculation took this amount of time: 4.730270 s
with memoization: the calculation took this amount of time: 0.001000 s
*/
内存缓存的技术在使用计算成本相对昂贵的函数时非常有用(不仅限于例子中的递归),譬如大量进行相同参数的运算。这种技术还可以应用于纯函数中,即相同输入必定获得相同输出的函数
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】博客园携手 AI 驱动开发工具商 Chat2DB 推出联合终身会员
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 欧阳的2024年终总结,迷茫,重生与失业
· 聊一聊 C#异步 任务延续的三种底层玩法
· 上位机能不能替代PLC呢?
· 2024年终总结:5000 Star,10w 下载量,这是我交出的开源答卷
· .NET Core:架构、特性和优势详解
2022-03-01 【npm】npm整个那么大的包管理体系竟然不在官网标出依赖版本
2019-03-01 Git无法提交,报错