函数可以为我们隐藏某一个分解的任务的细节实现,暴露出需要的参数和返回结果。
在 Go 语言中,如果一组形参或者返回值有相同的类型,我们不必为每个形参都写出参数类型。
func f(i int, j int, k int, s string, t string) {
/* ... */ }
===> 等价于
func f(i, j, k int, s, t string) {
/* ... */ }
在函数体中,函数的形参作为局部变量,被初始化为调用者提供的值。函数的形参和有名返回值作为函数最外层的局部变量,被存储在相同的词法块中。
//Go函数的返回值变量能被提前声明,并且作用于整个函数的区块内
func f(x,y int) (z int,a string) {
z = x + y
a = "OK"
return
}
fmt.Println(f(1,1)) //2 OK
实参通过值的方式传递,因此函数的形参是实参的拷贝。对形参进行修改不会影响实参。但是,如果实参包括引用类型,如指针,slice
、map
、function
、channel
等类型,实参可能会由于函数的间接引用被修改。
在Go中,函数像其他值一样,拥有类型,可以被赋值给其他变量,传递给函数,从函数返回。对函数值的调用类似函数调用一样,函数可以赋值给一个变量,然后通过变量来实现
func square(n int) int {
return n * n
}
func show(m,n int) int {
return m + n
}
f1 := square
fmt.Printf("%T\n",f1) //func(int) int
fmt.Println(f1(2)) //4
f2 := show
fmt.Printf("%T\n",f2) //func(int, int) int
fmt.Println(f2(3,12)) //15
函数值可以减少重复代码的使用,增加复用
package part1
//获取url网页内容的函数
func FetchUrl(urls []string) (s string){
for _, url := range urls {
resp, err := http.Get(url)
if err != nil {
log.Fatalf("fetch failed : %v\n",err)
}
byte, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
log.Fatalf("fetch reading content failed: %v\n",err)
}
s += string(byte)
}
return
}
var depth int
func startElement(n *html.Node) {
if n.Type == html.ElementNode {
fmt.Printf("%*s<%s>\n", depth*2, "", n.Data)
depth++
}
}
func endElement(n *html.Node) {
if n.Type == html.ElementNode {
depth--
fmt.Printf("%*s%s>\n", depth*2, "", n.Data)
}
}
//遍历节点之前,通过前置和后置函数处理添加空格
//pre函数和end函数都是可选的,并且作为参数传递
func forEachElement(n *html.Node,pre,end func(n *html.Node)) {
if pre != nil {
pre(n)
}
for c := n.FirstChild;c != nil;c = c.NextSibling{
forEachElement(c,pre,end)
}
if end != nil {
end(n)
}
}
func TestForEachElement(t *testing.T) {
var url = []string{
"https://www.baidu.cn"}
s := part1.FetchUrl(url)
doc, err := html.Parse(strings.NewReader(s))
if err != nil {
fmt.Printf("findlinks1: %v\n", err)
}
//这里直接传入startElement和endElement函数作为参数
forEachElement(doc,startElement,endElement)
}
拥有函数名的函数只能在包级语法块中被声明,通过函数字面量,我们可绕过这一限制,在任何表达式中表示一个函数值。函数字面量的语法和函数声明相似,区别在于func
关键字后没有函数名。函数值字面量是一种表达式,它的值被称为匿名函数。
把上面的square
函数来改写下
func square() func() int{
var x int
return func() int{
x++
return x*x
}
}
没有名称的函数func() int
作为函数square
的返回值,然后该匿名函数每次被调用的时候都会返回下一个数的平方,每次调用匿名函数时,该函数都会先使x的值加 1 ,再返回x
的平方。第二次调用square
时,会生成第二个x
变量,并返回一个新的匿名函数。新匿名函数操作的是第二个x
变量。
//测试调用square函数
func TestTwo(t *testing.T) {
f3 := square() //将该函数值赋予变量f3
fmt.Println(f3()) //1
fmt.Println(f3()) //4
fmt.Println(f3()) //9
fmt.Println(f3()) //16
fmt.Println(f3()) //25
}
在连续调用函数f3
之后,打印的值表明在匿名函数中x
递增的,函数square
和匿名函数之间存在着变量引用,函数值可以记录状态。也可以理解为为什么函数值是引用类型并且函数值无法比较。Go 使用闭包技术实现函数值,函数值也叫做闭包。
通过这个例子,我们看到变量的生命周期不由它的作用域决定:
square
返回后,变量x
仍然隐式的存在于f
中。
defer
后面的函数总是会在包含defer
的外围函数执行完毕后才会执行,也就是说延时执行,不论包含defer
语句的函数是通过return
正常结束,还是由于panic
导致的异常结束。你可以在一个函数中执行多条defer
语句,它们的执行顺序与声明顺序相反。
func TestDeferred1(t *testing.T) {
a, b := 1, 2
defer func() {
fmt.Println(a, b)
a = a * 3
b = 0
fmt.Println(a, b)
}()
a, b = 10, 20
fmt.Printf("----->%d\t%d\n", a, b)
}
//输出:
----->10 20
10 20
30 0
可以看到,一开始并没有按照顺序执行defer后面匿名函数的内容,而是执行了后面的赋值a, b = 10, 20
操作和打印,并且后续匿名函数的a,b
的值也不是初始化的 1 和 2,而是 10 和 20。
在匿名函数传参之后
func TestDeferred2(t *testing.T) {
a, b := 1, 2
defer func(a, b int) {
fmt.Println(a, b)
a = a * 3
b = 0
fmt.Println(a, b)
}(a, b)
a, b = 10, 20
fmt.Printf("----->%d\t%d\n", a, b)
}
//输出
----->10 20
1 2
3 0
在将参数传入到匿名函数之后,匿名函数保留着传入之前的值,不再受到延迟执行之前其他的变量赋值操作,等价于下面的:
func TestDeferred3(t *testing.T) {
a, b := 1, 2
defer sh(a,b)
a, b = 10, 20
fmt.Printf("----->%d\t%d\n", a, b)
}
func sh(a, b int) {
fmt.Println(a, b)
a = a * 3
b = 0
fmt.Println(a, b)
}
//输出
----->10 20
1 2
3 0
defer
语句经常被用于处理类似开始-关闭的操作,如打开、关闭、连接、断开连接、加锁、释放锁。通过defer
机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放。释放资源的defer
应该直接跟在请求资源的语句后。
resp, err := http.Get(url)
defer resp.Body.Close()
....