函数的基本形式
//函数定义。a,b是形参
func argf(a int, b int) {
a = a + b
}
var x, y int = 3, 6
argf(x, y) //函数调用。x,y是实参
- 形参是函数内部的局部变量,实参的值会拷贝给形参。
- 函数定义时的第一个的大括号不能另起一行。
- 形参可以有0个或多个。
- 参数类型相同时可以只写一次,比如argf(a,b int)。
- 在函数内部修改形参的值,实参的值不受影响。
如果想通过函数修改实参,就需要指针类型。
func argf(a, b *int) {
*a = *a + *b
*b = 888
}
var x, y int = 3, 6
argf(&x, &y)
传引用和传引用的指针
slice、map、channel都是引用类型,它们作为函数参数时其实跟普通struct没什么区别,都是对struct内部的各个字段做一次拷贝传到函数内部。(go 语言里面只有一条原则,只要是函数传递
参,都是传的拷贝)
比如slice作为函数参数,slice就是一个结构体,把这个结构体作为参数传递给函数,就是对结构体里面的每一个成员变量都进行一次拷贝。只不过结构体的成员变量包含指针。
下面可以看到修改的是底层的数组,可以影响到形参,但是append会影响,生成新的切片,那么就和原来的切片没有关系了。
package main
import "fmt"
func slice_arg_1(arr []int) { //slice作为参数,实际上是把slice的arrayPointer、len、cap拷贝了一份传进来
arr[0] = 1 //修改底层数据里的首元素
arr = append(arr, 1) //arr的len和cap发生了变化,不会影响实参
}
func main() {
arr := []int{8}
slice_arg_1(arr)
fmt.Println(arr[0]) //1
fmt.Println(len(arr)) //1
}
关于函数返回值
- 可以返回0个或多个参数。
- 可以在func行直接声明要返回的变量。
- return后面的语句不会执行。
- 无返回参数时return可以不写。
不定长参数
Go 语言中的可变长参数允许调用方传递任意多个相同类型的参数
• 函数定义
func append(slice []Type, elems ...Type) []Type
• 调用方法
myArray := []string{}
myArray = append(myArray, "a","b","c")
func variable_ength_arg(a int, other ...int) int {
sum := a
for _, ele := range other {//不定长参数实际上是slice类型
sum += ele
}
fmt.Printf("len %d cap %d\n", len(other), cap(other))
return sum
}
variable_ength_arg(1)
variable_ength_arg(1,2,3,4)
append函数接收的就是不定长参数。 在很多场景下string都隐式的转换成了byte切片,而非rune切片,比如"a中"[1]是228而非"中"。
arr = append(arr, 1, 2, 3)
arr = append(arr, 7)
arr = append(arr)
slice := append([]byte("hello "), "world"...)
//...自动把"world"转成byte切片,等价于[]byte("world")...
slice2 := append([]rune("hello "), []rune("world")...)
//需要显式把"world"转成rune切片
func test(slice ...int){
slice = append(slice,1)
fmt.Println(slice,len(slice),cap(slice))
fmt.Printf("%p\n",slice)
}
func main() {
slice1 := make([]int,6,6)
test(slice1...)
fmt.Printf("%p\n",slice1)
fmt.Println(slice1)
}
[0 0 0 0 0 0 1] 7 12
0xc000060060
0xc0000160f0
[0 0 0 0 0 0]
切片本质是结构体,本质上是对len改变了,改变len是改变了拷贝,并不影响原先那个,但是底层数组确实发生了改变。👇
x := make([]int,2,4)
fmt.Println(len(x),cap(x),x)
fmt.Printf("%p\n",x)
update(x)
func update(slice []int){
slice[0] = 0
slice[1] = 1
slice = append(slice,3,4)
fmt.Println(cap(slice),len(slice),slice)
fmt.Printf("%p",slice)
}
2 4 [0 0]
0xc0000b8020
4 4 [0 1 3 4]
0xc0000b8020
Main 函数
• 每个 Go 语言程序都应该有个 main package
• Main package 里的 main 函数是 Go 语言程序入口
go语言里面最重要的就是main函数,每个程序都需要有个程序入口,你要告诉底层的编译器说从何编译,然后运行程序的时候main函数告诉底层的编译器,如何编译,运行程序的时候先执行哪个方法,go语言做了一定的约定,任何的go语言程序首先要有一个main package,main package里面的main方法就是go语言的程序入口。
在编译的时候发现有一个main函数,那么就会构建一个可执行文件去运行这个程序。
package main
func main() {
args := os.Args
if len(args) != 0 {
println("Do not accept any argument")
os.Exit(1) }
println("Hello world")
}
参数解析
• 请注意 main 函数与其他语言不同,没有类似 java 的 []string args 参数
• Go 语言如何传入参数呢?
• 方法1:
l fmt.Println("os args is:", os.Args)
• 方法2:
l name := flag.String("name", "world", "specify the name you want to say hi")
l flag.Parse()
运行任何程序的时候,都需要给其一些参数,很多时候需要给程序一些入参,
init函数
有时候,在做程序运行之前,需要有一些初始化的动作,要给一些变量赋值,或者有预先要去运行的程序要去跑,这个时候就需要做一些初始化的操作。
init函数和main函数一样,也是一种特殊的函数,init函数会在main函数之前运行。
首先程序的入口是main方法,在main包去运行main方法的时候,会去做一系列的事情,首先会去看import了哪些包,如果发现main package引用了package1,那么程序就会跳转到package1,去做package1的初始化,也就是一个包有针对其他包依赖的话,先要跳转到其他包的初始化,直到最后一个包里面,最后一个包init方法结束了之后,回到引用的包还是常量变量初始化。
main方法当中还是以同样的顺序去初始化常量,变量,以及执行init方法,最后才执行main。
包中含有init方法,包被多个包所依赖,那么init方法只会被执行一次。
package main
import (
"fmt"
_ "github.com/cncamp/golang/examples/module1/init/a"
_ "github.com/cncamp/golang/examples/module1/init/b"
)
func init() {
fmt.Println("main init")
}
func main() {
fmt.Println("main function")
}
init from b
init from a
main init
main function
返回值
• 多值返回
• 函数可以返回任意数量的返回值
• 命名返回值
• Go 的返回值可被命名,它们会被视作定义在函数顶部的变量。
• 返回值的名称应当具有一定的意义,它可以作为文档使用。
• 没有参数的 return 语句返回已命名的返回值。也就是直接返回。
• 调用者忽略部分返回值
result, _ = strconv.Atoi(origStr)
内置函数
go语言提供了内置的函数,go语言保留的关键字其实非常的少,它的内置函数也不多,如上所示。
回调函数(Callback)
函数本身可以是变量,那么这个函数可以作为参数传给另外一个函数,在另外一个函数里面去调用这个参数函数。上面可以看到可以将函数名字作为变量传到了行参。
所谓的回调函数就是在函数体里面,入参传了一个函数进来,然后在函数体里面调用了这个被传进来的参数。
闭包
匿名函数,没有名字,可以在定义的时候就直接运行了。
什么场景下会使用匿名函数呢?也就是闭包。一个最常用的场景就是程序运行出现错误的时候,我们要去恢复,一般是通过关键字defer,defer在函数退出的时候执行,我们要有一个程序逻辑,在某些特定场景下运行的,但是我没有必要为整个逻辑定义函数,通常这样就可以使用闭包来做。
传值还是传指针
• Go 语言只有一种规则-传值
• 函数内修改参数的值不会影响函数外原始变量的值
• 可以传递指针参数将变量地址传递给调用函数,Go 语言会复制该指针作为函数内的地址,但指向同一地址
go语言在传递指针参数的时候,在go语言函数内部,它一样会复制这个指针地址,这两个变量指向的是同一个内存地址,所以在函数体里面去修改指针变量的时候,它其实是修改了那块内存。
从函数体外面来看就感觉那个变量发生了变化。
• 思考:当我们写代码的时候,函数的参数传递应该用 struct还是 pointer?
这个需要分场景了,如果是使用指针,好处是传递的是指针地址,就涉及不到一些值的拷贝。从效率上来看这种情况可能会高一些,但是如果你传递的是struct,那么这个struct传递到函数体里面以后,这个函数一退出,之前的那些临时变量就立马可以被回收了。它对GC更加友好一些。