Go语言函数

go不支持函数重载

go的函数没有顺序要求,不像C++要先声明了函数后面的代码才能用,go中要调用的函数的声明在当前代码块的后面也可以执行。

函数写法:

func function_name( [parameter list] ) [return_types] {
   函数体
}

若函数不返回值,则可以不需要返回类型,类似于void

函数可以返回多个值,可以有多个返回类型:

func test(a,b int,c,d string) (int, string) {
	return 1, "yes"
}// 形参a,b为int,c,d为string,注意这里的类型判断逻辑

**值传递和C++一样,**其实go中存在的都是值传递,只不过如果传的是个指针或者地址,那么表现上就像引用传递一样,能够在函数内部修改外部传进来的变量

引用传递,传的是变量的地址,因此在引用传递下的函数形参是指针类型。

slice,map,channel 会有传引用的错觉(比如切片,他背后对应的是一个数组,切片本身是一个数据结构,在这个数据结构中包含了指向了这个数组的指针。所以说,即便是在传值的情况下这个结构被复制到函数里了,在通过指针去操作这个数组的值的时候,其实是操作的是同一块空间,实际上是结构被复制了,但是结构里包含的指针指向的是同一个数组,所以才有这个错觉)

与C++不同,Go语言的函数返回值可以是局部对象的指针,这是一个很大的区别。:

func ret_pointer() (*int){
	i:=123
	return &i
}
func main(){
	ptr:=ret_pointer()
	fmt.Println(*ptr)
} //output 123

对于有多个返回值的函数,例如range一个数组,或者返回错误码和错误信息的函数等等,我们可以选择性的接收返回值的个数:
只接收第一个返回值:

func test_func1() (int,string){ 
/* */ 
}
func main(){
	ret:=test_func1()
}

可以通过下划线忽略返回值列表中的某个返回值:

func test_func2() (int,string,float32){ 
/* */ 
}
func main(){
	ret_int,_,ret_float:=test_func1()
}

******

函数返回值如果指定了名字,Go会在进入函数后,执行函数代码前,初始化这些命名了的返回值为其类型对应的零值,此时,如果函数的return没有跟任何东西,则Go会自动把return时的命名返回值返回给调用者:

func namedFunc() (n int, s string){
	n=123
	s="helloworld"
	return 
}
func main(){
	i,s:=namedFunc()
	fmt.Println(i,s)
}//output 123 helloworld

所以这种使用命名了的返回值的情况下,要么return后不跟东西,Go自动帮你返回你指定名字的返回值,要么你return+命名的返回值,以上面的代码为例,namedFunc函数中的另一种返回方式为:return n,s

******


闭包

普通函数是静态的,而闭包是动态的,只有在运行时才知道这个闭包到底引用了什么变量,变量的值多少

函数闭包可以简单的理解成定义在一个函数体内的函数,闭包函数可以引用其外部函数的变量,每调用一次外部函数就生成一个单独的闭包,互不影响

image

通过调用外部的函数,得到一个闭包函数,这个闭包函数引用了这个外部函数的对象的一份副本,每个闭包函数各自独立,各自是一个包一样。

func test2() func() int {
	i := 1
	return func() int {
		i++
		return i
	}
}
func main(){
	clp1 := test2()
	fmt.Println(clp1())
	fmt.Println(clp1())
	clp2 := test2()
	fmt.Println(clp2())
	fmt.Println(clp2())
}//输出
2 
3
2 
3

不一定说必须在函数内部定义的函数才可能是闭包(其实就是外部变量对于内部函数而言是可见的,所以可以引用):

func main() {

	for i:=0; i<5; i++ {
		go func(){
			fmt.Println(i) 
		//i变量值也是引用.创建5个线程执行函数,线程刚好处于i的某个值,输出的值不确定
		}()
		
	}
	time.Sleep(time.Second * 1)
}

函数方法

Go语言没有面向对象。

可以给所有的自定义名字的类型添加方法,除了接口和指针类型外。

The rule about pointers vs. values for receivers is that value methods can be invoked on pointers and values, but pointer methods can only be invoked on pointers.

规则:以值类型作为接收者的方法既可以通过对象的值也可以通过对象的指针触发,但指针类型作为接收者的时候,就只能通过对象的指针触发。这是因为值接收者在方法里其实操作的是对象的副本,不管是类型的指针还是值,都没影响;而指针接收者要修改对象的值,所以存在一定的区别。

但是如果值对象可以取址,那么通过它调用指针接收者的方法也是可以的,否则一定要按照这个规则来:

type newStruct struct {
	a, b int
}
func (ns *newStruct) test_pointer() {
	fmt.Println("invoked by pointer receiver method")
}
func (ns newStruct) test_no_pointer() {
	fmt.Println("invoked by no pointer receiver method")
}
func main(){
	ns:=newStruct{123,456}  //类型的值
	ns.test_no_pointer() //调用值接收者的方法,当然没问题
    ns.test_pointer()    //调用指针接受者的方法,只要对象可以取址,编译器会自动补上&后再调用,也即编译器优化后实际执行的是(&ns).test_pointer()
	
	nns:=new(newStruct) //类型的指针
	nns.test_no_pointer() //调用值接收者的方法,值接收者的方法可以使用类型的指针进行调用
	nns.test_pointer()   //调用指针接收者的方法,当然没问题
	
}

函数方法就是通过编写一个函数,与某一类型相关联,然后在后续的函数调用上,如同该类型的成员方法一样:

func (variable_name variable_data_type) function_name() [return_type]{
   /* 函数体*/
}

//example
type mystruct struct{
	radius float32
}
func (c mystruct) getRadius() float32{
	return c.radius
}
func main(){
	var Test mystruct
	Test.radius=123.456
	fmt.Println(Test.getRadius)//如同mystruct类型的成员方法一样
}//输出 123.456

如果想要修改类型的值,用该类型的指针来编写:

//example
type mystruct struct{
	radius float32
}
func (c *mystruct) setRadius(newRadius float32){
	c.radius=newRadius //注意这里,虽然是指针但是还是用了句号运算符
    //在C++中指针访问成员是用箭头运算符,但这里不一样,多加注意
}
func main(){
	var Test mystruct
	Test.radius=123.456
    Test.setRadius(555.666)
	fmt.Println(Test.radius)//如同mystruct类型的成员方法一样
}//输出 555.666

如果不是使用指针的方法来编写类型的方法,一方面是无法实现修改对象的效果,另一方面是每次都是值传递,操作的都是这个对象的副本,会存在开辟新的内存的情况,个人认为,即使方法不修改对象的值,也最好用指针的方式,减少内存的创建和销毁:

type MyStruct struct {
	radius float32
	height float32
}

func (m *MyStruct) ModifyP() {
	m.radius = 10
	m.height = 20
}

func (m MyStruct) Modify() {
	m.radius = 10
	m.height = 20
}

func PrintMystruct(m *MyStruct) {
	fmt.Println("radius: ", m.radius)
	fmt.Println("height: ", m.height)
}

func main() {
	ms := MyStruct{1.2, 2.4}
	PrintMystruct(&ms)
	ms.Modify()
	PrintMystruct(&ms)
	ms.ModifyP()
	PrintMystruct(&ms)
}
//output
radius:  1.2
height:  2.4
radius:  1.2
height:  2.4
radius:  10
height:  20

对于采用值和指针作为接收者的两种编写方法的方式,最完整的调用方法的方式如下,但指针接收者也可像值接收者那样,不需要取址后再调用方法,我们编写m.test2(),go编译器会自动解释为(&m).test2()。

func (m Mystruct) test1(){
	/* code */
}
func (m *Mystruct) test2(){
	/* code */
}
func main(){
	m:=Mystruct()
	m.test1()
	(&m).test2() //指针接收者的最完整调用方法的方式,也可以像上面那行那样调用,也是没问题的
}

不能给基本变量类型(内置类型)添加函数方法**,只能给自定义的类型添加函数方法。但是可以使用类似typedef一样的效果,给内置变量取别名,然后再对该别名声明方法:

type MyInt int
func(i MyInt) test() Myint{ //要注意虽然MyInt本质上就是int,但是两种类型的变量不能赋值,所以这里不能用int做返回类型
	return i*i
}
func main(){
    i:=MyInt(2)
    fmt.Println(i.test())
}//output 4

只能给同一个包里的定义的类型编写方法,不能给其他包内的类型写方法,所以这也是为什么不能给内置类型添加方法的原因,因为内置变量的定义在其他包内。


变量作用域

作用域基本规则与C++一样,可以用大括号来划分作用域。

  • 函数内定义的变量称为局部变量

  • 函数外定义的变量称为全局变量

  • 函数定义中的变量称为形式参数

    局部变量和全局变量默认初始化为零值。

局部变量

函数内的变量、形参、返回值都是局部变量

全局变量

函数体外声明的变量为全局变量。一个包里的对象想要别的包可见的话,需要首字母大写;而如果是同一个包的不同文件,则直接使用对象即可,不区分首字母大小写的问题,但是在运行时需要告诉编译器是包的哪几个文件:

hello.go引用了test.go的函数和变量,运行代码应为:go run test.go hello.go (两个文件谁前谁后无所谓)

image

全局变量可以与局部变量同名,但内部的局部变量会屏蔽外部的同名变量