【Go】函数与方法
文章目录
函数
1、函数定义
1、golang函数特点:
1)无需声明原型
2)支持不定参数
3)支持多返回值
4)支持命名返回参数
5)支持匿名函数和闭包
6)函数也是一种类型,可以赋值给变量
7)不支持 嵌套(nseted)一个包不能有两个名字一样多函数
8)不支持 重载(overload)
9)不支持 默认参数(default parameter)
2、函数声明:
1)函数声明包含一个函数名、参数列表、返回值列表和函数体
;如果函数没有返回值,则返回列表可以忽略,函数从第一条语句开始执行,直到执行return语句或者执行函数的最后一条语句。
2)函数可以没有参数 或 多个参数
3)注意类型在变量名之后
4)函数可以返回任意数量的返回值
func test(x,y int, s string) (int, string) {// 类型相同的相邻参数可以合并,多返回值必须使用括号n := x + yreturn n, fmt.Println(s, n)
}例子:
package mainimport "fmt"func test(fn func() int) int {return fn()
}
// 定义函数类型。
type FormatFunc func(s string, x, y int) string func format(fn FormatFunc, s string, x, y int) string {return fn(s, x, y)
}func main() {s1 := test(func() int { return 100 }) // 直接将匿名函数当参数。s2 := format(func(s string, x, y int) string {return fmt.Sprintf(s, x, y)}, "%d, %d", 10, 20)println(s1, s2)
}结果:100, 10 20
1、有返回值的函数,必须有明确的终止语句,否则会引发编译错误
2、没有函数体的函数声明,表示该函数不是以Go实现的,这样的声明定义了函数标识符
2、参数
函数定义的时候有参数,成为形参,就像定义在函数体内的局部变量,但当调用函数,传递过来的变量就是函数的实参,两种方式传递参数:
1)值传递:实际参数复制一份传递到函数中,不会影响到实际参数
2)引用传递:调用函数时候将实际参数地址传递到函数中,在函数中对参数进行修改,将影响到实际参数
默认情况下,Go语言使用的是值传递,调用过程中不会影响到实际参数! => map、slice、chan、指针、interface默认以引用的方式传递
不论值传递还是引用传递,传递给函数的都是变量的副本,不过值传递的是值的拷贝,引用传递,传递的是地址的拷贝,一般来说,地址拷贝更加高效,值拷贝取决于对象大小
1、不定参数传值
func myfunc(args ...int) { // 0或多个参数}
func myfunc(a int, args ...int) { // 1或多个参数}
func myfunc(a int, b int, args ...int) { // 2或多个参数}// 注意:其中args是一个slice,可以通过args[index]依次访问所有参数,通过len(arg)来判断传递参数的个数
2、使用interface()传递任意数据类型参数,且interface{}是类型安全的
func myfunc(args ...interface{}) {}
3、返回值
"_"标识符,用来忽略函数的某个返回值
1)没有参数的return语句,返回各个变量的当前值,成为"裸"返回 => 长函数中会影响代码可读性
2)golang返回值不能用容器对象接收多返回值,只能用多个变量,或"_"忽略
3)命名返回参数允许defer延迟调用通过闭包读取和修改
package mainfunc add(x, y int) (z int) {defer func() {z += 100}z = x + yreturn
}func main() {println(add(1, 2))
}// 结果:103
1、defer是go中的一种延迟机制,defer后面的函数只有在当前函数执行完毕之后才能执行,通常用于释放函数
2、defer遵循先入后出,类似于栈的结构
3、为什么?因为后申请的资源可能对前面申请的资源有依赖性,如果将前面的资源释放掉,对后面的资源可能造成影响
4、什么时候执行defer?1)将返回值复制给一个变量;2)执行RET执行 => defer执行在1之后,在2之前
4、匿名函数
匿名函数是指不需要定义函数名的一种实现方式,在Go中,函数可以像普通变量一样传递或使用,Go语言支持随时在代码里定义匿名函数(不带函数名的函数声明 + 函数题)
package mainimport ("fmt""match"
)func main() {getSqrt := func(a float64) float64 {return math.sqrt(a)}fmt.Println(getSqrt(4))
}// 输出2
5、闭包、递归
1、闭包详解
闭包是由函数及其相关引用环境组合而成的实体(闭包 = 函数 + 引用环境)
<!DOCTYPE html>
<html lang="zh">
<head><title></title>
</head>
<body>
</body>
</html>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.2.6/jquery.min.js" type="text/javascript"></script>
<script>
function a(){var i=0;function b(){console.log(++i);document.write("<h1>"+i+"</h1>");}return b;
}$(function(){var c=a();c();c();c();//a(); //不会有信息输出document.write("<h1>=============</h1>");var c2=a();c2();c2();
});</script>
1)函数b嵌套在函数a的内部;2)函数a返回函数b;3)执行完var c = a()后,变量c实际上指向函数b,再执行函数c()就会显示i的值 => 创建一个闭包,函数a()外的变量c引用函数a()内的函数b()
简单说:函数a()的内部函数b(),被函数a()外的一个变量引用的时候,就创建一个闭包
2)作用:在a()执行完之后,闭包使得垃圾回收机制GC不会回收a()所占用的资源,因为a()的内部函数b()的执行需要依赖a()中的变量i
2、Go语言的闭包 => 闭包复制的是原对象的指针,很容易解释延迟引用现象
package mainimport ("fmt"
)func a() func() int {i := 0b := func() int {i++fmt.Println(i)return i}return b
}func main() {c := a()c()c()c()a() //不会输出i
}// 输出1 2 3
3、Go语言递归函数
递归就是在运行过程中调用自己,一个函数调用自己,就叫做递归函数。条件:
1)子问题必须与原问题做同样的事情,且更简单
2)不能无限制地调用本身,须有个出口,化简为非递归状况处理
6、Golang延迟调用defer(❌)
1、defer特性:
1)关键字defer用于注册延迟调用
2)这些调用直到return前才被执行,因此,可以用来做资源管理
3)多个defer语句,按先进后出的方式执行
4)defer语句中的变量,在defer声明时就决定了
2、用途:
1)关闭文件句柄
2)锁资源释放
3)数据库连接释放
1、用来做逆序输出!
3、defer + 闭包(❌)
func main() {var whatever [5]struct{}for i := range whatever {defer func() { fmt.Println(i) }()}
}
// 输出 4 4 4 4 4
函数正常执行,由于闭包用到的变量i在执行的时候已经变成4,所有输出全部是4
4、defer f.Close
// 方式一
type Test struct {name string
}func (t *Test) Close() {fmt.Println(t.name, " closed")
}func main() {ts := []Test{{"a"}, {"b"}, {"c"}}for _, t := range ts {defer t.Close()}
}
// 输出结果 c closed(3个) 为什么不是c b a// 方式二
func Close(t Test) {t.Close()
}func main() {ts := []Test{{"a"}, {"b"}, {"c"}}for _, t := range ts {defer Close(t)}
}//输出结果 c b a
7、异常处理(❌)
Golang没有结构化异常,使用pannic抛出异常,recover捕获错误。 => Go中可以抛出一个panic异常,然后在defer中通过recover捕获这个异常,然后正常处理。
panic:
1、内置函数
2、假如函数F中书写了panic语句,会终止其后要执行的代码,在panic所在函数F内如果存在要执行的defer函数,按照defer的逆序执行
3、返回函数F的调用者G,在G中,调用函数F语句之后的代码不会执行,假如函数G中存在要执行的defer函数列表,按照defer的逆序执行
4、直到goroutine整个退出,并报告错误
recover:
1、内置函数
2、用来控制一个goroutine的panicking行为,捕获panic,从而影响应用的行为
3、一般的调用建议:1)在defer函数中,通过recover来终止一个goroutine的panicking过程,从而恢复正常代码的执行2)可以捕获通过panic传递的error
注意:
1、利用recover处理panic指令,defer必须放在panic之前定义,另外recover只有在defer调用的函数中才有效,否则当panic时,recover无法捕获到panic,无法仿制panic扩散
2、recover处理异常后,逻辑不会恢复到panic那个点去,函数跑到defer之后的那个点
3、多个defer会形成defer栈,后定义的defer会被最先调用
8、单元测试
8.1、go test工具
go test命令时一个按照一定约定和组织的测试代码的驱动程序,在包目录内,所有以_test.go为后缀名的源代码文件都是go test测试的一部分,不会被go build编译到最终的可执行文件中。
在xxx_test.go
中有三种类型的函数:单元测试函数、基准测试函数和示例函数
1、测试函数:前缀名为Tset,测试程序的逻辑行为是否正常
2、基准函数:前缀名为Benchmark,测试函数的性能
3、示例函数:前缀名为Example,为文档提供示例代码
go test命令会遍历所有的_test.go文件中符合👆命名规则的函数,生成一个临时的main包用于调用相应的测试函数,然后构建并运行,报告测试结果,最后清理生成的临时文件。
1、文件名必须以xxx_test.go命名
2、方法必须是Test[^a-z]开头
3、方法参数必须 t *testing.T
4、使用go test执行单元测试
8.2、测试函数
每个测试函数必须导入testing包,测试函数的基本格式:
func TestName(t *testing.T) {// ...
}// 由这个例子可以看出来:1)测试函数比如Test开头;2)参数必须是 t *testing.T;3)testing.T也就是参数t用于报告测试失败和附加的日志信息
8.3、测试函数示例
package splitimport "strings"func Split(s, sep string) (result []string) {i := strings.Index(s, sep) // 查找sep在s中的索引for i > -1 { // 找到的话result = append(result, s[:i]) // 不被包含的那一部分加入到result中s = s[i+1:] // 对s进行重新赋值i = strings.Index(result, s) }result = append(result, s)return
}
8.4、压力测试
Go语言自带一个轻量级的测试框架testing和自带的go test命令来实现单元测试和性能测试,建议安装gotests
插件自动生成测试代码 => go get -u -v github.com/cweill/gotests/...
1、如何编写测试用例
1)新建一个项目目录gotest,这样我们所有代码和测试代码都在这个目录下
2)创建两个文件:gotest.go和gotest_test.go
2、如何编写压力测试
压力测试必须遵循这样的格式:func BenchmarkXXX(b *testing.B) {...}
1)go test默认不会执行压力测试的函数,如果需要的话使用 go test -test.bench="xxx"
方法
1、方法定义
Golang方法总是绑定对象实例,并隐式将实例作为第一实参(receiver)
1、只能为当前包内类型定义方法
2、参数 receiver 可以任意命名,如方法中未曾使用,可以省略参数名
3、参数 receiver 类型可以是T 或 *T,基类型T不能是接口或指针
4、不支持方法重载,receiver只是参数签名的组成部分
5、可用实例,value或pointer调用全部方法,编译器自动转换
一个方法就是包含了接受者的函数,接受者可以是命名类型或结构类型的一个值或一个指针
func (receiver Type) methodName(参数列表)(返回值列表){}
=> 参数和返回值可以省略
1、例子
package mainimport "fmt"// User 结构体
type User struct {Name stringEmail string
}// Notify 方法
func (u User) Notify() {fmt.Printf("%v : %v \\n", u.Name, u.Email)
}
func main() {// 值类型调用方法u1 := User{"chenzhihui","czh1074@163.com"}u1.Notify()// 指针类型调用方法u2 := User{"hyt","xxx.com"}u3 := &u2u3.Notify()
}// 解释:首先,定义一个叫User的结构体,定义一个该类型的方法叫Notify,该方法的接受者是一个User类型的值,要调用Notify方法需要一个User类型或者指针
2、普通函数与方法的区别
1)普通函数:接收者为值类型的时候,不能将指针类型的数据直接传递,反之亦然
2)方法:接收者为值类型的时候,可以直接用指针类型的变量调用方法,反过来同样也可以
2、匿名字段
Golang匿名字段:可以像字段成员那样访问匿名字段,编译器负责查找
package mainimport "fmt"type User2 struct {Name stringAge int
}type Manager struct {User2
}func (u2 *User2) ToString() string {return fmt.Sprintf("User2: %p, %v\\n", u2, u2) // 通过匿名字段,获得类似继承的能力
}func main() {m := Manager{User2{"czh",18}}fmt.Printf("Manager : %p\\n", &m)fmt.Println(m.ToString())
}结果:
Manager : 0x14000114018
User2: 0x14000114018, &{czh 18}
3、方法集(❌:作用是什么呢?)
Golang方法集:每个类型都有与之关联的方法集,这会影响到接口实现规则
1、类型T方法集包含全部 receiver T 方法
2、类型*T方法集包含全部receiver T、*T方法
3、如类型S包含匿名字段T,则S和*S方法集包含T方法
4、如类型S包含匿名字段*T,则S和*S方法集包含T、*T方法
5、不管嵌入T 或 *T、*S方法集总是包含T、*T方法
4、表达式
Golang表达式:根据调用者不同,方法分为2种表现形式 -> instance.method(args…) 或者 .func(instance, args…),前者称为method value、后者称为method expression。
区别在于:method value绑定实例、method expression须显式传参
5、自定义error
1、返回异常
package mainimport ("errors""fmt"
)func getCircleArea(radius float32) (area float32, err error) {if radius < 0 {// 构建个异常对象err = errors.New("半径不能为负")return}area = 3.14 * radius * radiusreturn
}func main() {area, err := getCircleArea(3)if err != nil {fmt.Println(err)} else {fmt.Println(area)}
}
2、自定义error
package mainimport ("fmt""os""time"
)type PathError struct {path stringop stringcreateTime stringmessage string
}func (p *PathError) Error() string {return fmt.Sprintf("path=%s \\nop=%s \\ncreateTime=%s \\nmessage=%s", p.path,p.op, p.createTime, p.message)
}func Open(filename string) error {file, err := os.Open(filename)if err != nil {return &PathError{path: filename,op: "read",message: err.Error(),createTime: fmt.Sprintf("%v", time.Now()),}}defer file.Close()return nil
}func main() {err := Open("/Users/5lmh/Desktop/go/src/test.txt")switch v := err.(type) {case *PathError:fmt.Println("get path error,", v)default:}}
输出结果:
get path error, path=/Users/pprof/Desktop/go/src/test.txt
op=read
createTime=2018-04-05 11:25:17.331915 +0800 CST m=+0.000441790
message=open /Users/pprof/Desktop/go/src/test.txt: no such file or directory