Go函数和方法

 一、函数

    函数是基本的代码块,用于执行一个任务。

    go语言至少有个main()函数

    1)函数定义

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

      func:声明这是一个函数
      function_name:函数名称,函数名和参数列表一起构成了函数签名
      parameter list:参数列表,注意类型在变量名之后
      return_types:返回类型,不是必须的,当没有返回值时,可以不指定返回类型,也可以返回多个值,如(string,string)
      函数体:函数定义的代码集合

    

     常用函数用法:

// 函数多参无返回值
func func_name(a,b int, c string){}
// 函数无参无返回值
func func_name(){}
// 单个返回值
func func_name(s string) string{}
// 多个返回值
func func_name (s string) (string,int){}
// 命名返回参数
func func_name(s string) (result string){
    ...
    result=1
    return
}
// 可变参数,可变参数只能做为函数参数存在,并且是最后一个参数,本质上是slice
func func_name(s string,args ...int){}

// 匿名函数,调用:f(1,2)
f := func(x,y int) int {
    return x + y
}

     注意:

      ①:Go函数不支持重载

      ②:一个包中不能有两个名字一样的函数

      ③:当两个或多个连续的函数命名参数是同一类型,则除了最后一个类型之外,其他的类型可以省略

      ④:函数可以返回任意数量的返回值

      ⑤:使用关键字 func 定义函数,左大括号依旧不能另起一行

 

  

     2)函数参数

      函数如果使用参数,该变量可称为函数的形参。

      形参就像定义在函数体内的局部变量。

      调用函数,传递过来的变量就是函数的实参,可以通过两种方式来传递参数:

传递类型 描述
值传递 值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
引用传递 引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

      默认情况下,Go 语言使用的是值传递,即在调用过程中不会影响到实际参数

      注意:

        ①:无论是值传递,还是引用传递,传递给函数的都是变量的副本,不过,值传递是值的拷贝。引用传递是地址的拷贝,一般来说,地址拷贝更为高效。而值拷贝取决于拷贝的对象大小,对象越大,则性能越低

        ②:map、slice、chan、指针、interface默认以引用的方式传递

 

      可变参数:

        Golang 可变参数本质上就是 slice。只能有一个,且必须是最后一个。

         在参数赋值时可以不用用一个一个的赋值,可以直接传递一个数组或者切片,特别注意的是在参数后加上“…”即可

      任意类型的不定参数:

        就是函数的参数和每个参数的类型都不是固定的

        用空接口:interface{}传递任意类型数据是Go语言的惯例用法,而且interface{}是类型安全的

 

 

    3)递归函数

      递归,就是在运行的过程中调用自己

      Go语言支持递归,注意在使用递归时,要设置退出条件,避免陷入死循环

func recursion() {
   recursion() /* 函数调用自身 */
}

func main() {
   recursion()
}

      构成递归需具备的条件

        ①:子问题须与原始问题为同样的事,且更为简单

        ②: 不能无限制地调用本身,须有个出口,化简为非递归状况处理

 

 

    4)defer

      Go语言的 defer 语句会将其后面跟随的语句进行延迟处理

      在 defer 归属的函数即将返回时,将延迟处理的语句按 defer 的逆序进行执行,也就是说,先被 defer 的语句最后被执行,最后被 defer 的语句,最先被执行。

      关键字 defer 的用法类似于面向对象编程语言 Java 和 C# 的 finally 语句块,它一般用于释放某些已分配的资源,典型的例子就是对一个互斥解锁,或者关闭一个文件

      defer关键字特性:

        ①:关键字 defer 用于注册延迟调用

        ②:这些defer调用直到所在函数 return 前才被执行。因此,可以用来做资源清理

        ③:多个defer语句,defer 所在的函数返回后,将按照后进先出的顺序执行 defer 保存的延迟调用函数,也就是说,后定义的defer函数先执行

        ④:defer 延迟调用函数可以读取并分配给返回函数的命名返回值

        ⑤:defer语句中的变量,在defer声明时就决定了

 

      defer用途:

        ①:关闭文件句柄

        ②:锁资源释放

        ③:数据库连接释放

        ④:panic捕获

 

 

    5)init函数和mian函数

      ①:init函数

        go语言中init函数用于包(package)的初始化,该函数是go语言的一个重要特性

        init函数是用于程序执行前做包的初始化的函数,比如初始化包里的变量等

        每个包可以拥有多个init函数,包的每个源文件也可以拥有多个init函数

        对同一个go文件的init()调用顺序是从上到下的。

        对同一个包中不同文件是按文件名字符串比较“从小到大”顺序调用各文件中的init()函数

        不同包的init函数按照包导入的依赖关系决定该初始化函数的执行顺序

        init函数不能被其他函数调用,而是在main函数执行之前,自动被调用

      ②:main函数

        Go语言程序的默认入口函数(主函数)

    func main(){
        //函数体
    }

 

    补充:go包初始化

      ①:包的初始化是按照包在程序中导入的顺序来进行,依赖顺序优先,每次初始化一个包。如果包b导入了包a,那么a在b之前就已经完成初始化。runtime需要解析包依赖关系,没有依赖的包最先初始化

      ②:初始化过程是自下而上的,main包最后初始化。所以在main函数开始执行之前,所有的包已经初始化完毕

      ③:包的初始化从初始化包级别的变量开始,这些变量按照声明顺序初始化,依赖顺序优先

      ④:如果包由多个.go文件组成,go工具在调用编译器前会将这些.go文件进行排序,那初始化会安装编译器收到的文件顺序进行

      ⑤:init函数在一个 package 中的所有全局变量都初始化完成之后, 才开始运行。init函数只会运行一次, 即使被 import 了很多次

        同一个 package 或源文件中, 可以有很多个init函数,同一个 Package 下的多个源文件中都有init函数也可以

        同一个源文件中, 写在更靠近文件上面的 init 函数更早运行

        同一个 package 中, 文件名排序靠前的文件中的 init 函数更早运行

        建议将init 函数就放在源文件的最上面

        如果一个 package 只有一个init函数, 那尽量放在和 package 同名的源文件里

 

    6)函数表达式

      使用函数表达式实现三目运算

    // 函数表达式,实现三目运算
    // 格式:func() returnType {...}()
    i := 1
    j := 2
    k := func() int {
        if i > j {
            return i
        }
        return j
    }()
    fmt.Println(k)

 

      带参数的函数表达式:

    h := func(a, b int) int {
        if a > b {
            return a
        }
        return b
    }(i, j)

 

 

    7)匿名函数

      匿名函数是指不需要定义函数名的一种函数实现方式,匿名函数由一个不带函数名的函数声明和函数体组成。

      匿名函数的优越性在于可以直接使用函数内的变量,不必声明

      在Go里面,函数可以像普通变量一样被传递或使用。Golang匿名函数可赋值给变量,做为结构字段,或者在 channel 里传送。

      如:

    getSqrt := func(a float64) float64 {
        return math.Sqrt(a)
    }
    fmt.Println(getSqrt(4))

      上面先定义了一个名为getSqrt 的变量,初始化该变量时和之前的变量初始化有些不同,使用了func,func是定义函数的,可是这个函数和上面说的函数最大不同就是没有函数名,也就是匿名函数。这里将一个函数当做一个变量一样的操作

      

 

    8)闭包

      什么是闭包?闭包就是能够读取其他函数内部变量的函数。

      通常只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。在本质上,闭包是将函数内部和函数外部连接起来的桥梁

       Go语言支持闭包,通过匿名函数的方式,如:

// 创建函数a,返回另外一个函数。该函数的目的是在闭包中递增i的变量
func a() func() int {
    i := 0
    b := func() int {
        i++
        fmt.Println(i)
        return i
    }
    return b
}

func main() {
    c := a()
    c() // 1
    c() // 2
    c() // 3

    a() //不会输出i
}

 

 

    9) new和make函数

      在Go语言中对于引用类型的变量,我们在使用的时候不仅要声明它,还要为它分配内存空间,否则我们的值就没办法存储。而对于值类型的声明不需要分配内存空间,是因为它们在声明的时候已经默认分配好了内存空间。要分配内存,就引出来今天的new和make。 Go语言中new和make是内建的两个函数,主要用来分配内存

      

      ①:new

        new函数的签名:  func new(Type) *Type

        Type表示参数类型,*type表示类型指针,new函数返回一个指向该类型内存地址的指针

        new函数不太常用,使用new函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值。

        如var a *int只是声明了一个指针变量a但是没有初始化,指针作为引用类型需要初始化后才会拥有内存空间,才可以给它赋值。

        应该按照如下方式使用内置的new函数对a进行初始化之后就可以正常对其赋值了:

func main() {
    var a *int
    a = new(int)
    *a = 10
    fmt.Println(*a)
}

      new引发panic异常:

//定义一个结构体,age字段为指针
type Student struct {
    age *int
}

//获取结构体对象指针
func getStudent() *Student {
    s := new(Student) // panic,因为new只会为结构体Student申请一片内存空间,不会为结构体中的指针age申请内存空间
    return s
}

 

      ②:make

        make函数的签名:func make(t Type, size ...IntegerType) Type

        make也是用于分配内存的,区别于new,它只用于slice、map以及channel的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没必要返回他们的指针了。

        make函数是无可替代的,我们在使用slice、map以及channel的时候,都需要使用make进行初始化,然后才可以对它们进行操作

 

      new函数和make函数的区别

        相同点:

          底层都是通过mallocgc申请内存

        不同点:

          ①:make 返回值是”引用类型“,new 返回值是指针类型

          ②:make仅用于初始化 slice,map 和 chan;new 可用于初始化任意类型(new并不常用) 

        

 

      

 

 

 二、方法

    Go 语言中同时有函数和方法。一个方法就是一个包含了接收者的函数。

    方法可以将类型和方法封装在一起,实现强耦合。

    接收者可以是命名类型或者结构体类型的一个值或者是一个指针。所有给定类型的方法属于该类型的方法集

    Go语言中的方法时一种作用于特定类型变量的函数。这种特定类型变量叫做接收者,接收者的概念类似于Java语言中的this,和Python语言中的self。只不过Go语言中需要将this显式的声明出来。

 

    方法的定义格式如下:

    func (接收者变量 接收者类型) 方法名(参数列表) (返回类型) {
       方法体
    }

    方法示例:

// 定义结构体
type User struct {
    name    string
    gender  string
    address string
    age     int
}

// 接收User类型的方法
// 值类型的接收者
func (user User) getName() string {
    return user.name
}

// 指针类型的接收者
func (user *User) setName(name string) {
    user.name = name
}

func main() {
    user := &User{
        name:    "yangyongjie",
        age:     27,
        gender:  "male",
        address: "nanjing",
    }
    name := user.getName() // 该方法只能User结构体类型的变量或指针才能调用
    fmt.Println(name)      // yangyongjie

    user1 := &User{}
    user1.setName("yyj")
    fmt.Println(user1.name) // yyj

}

 

    值类型的接收者和值类型的接收者方法的区别:

      指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。这种方式十分接近于Java语言中的this,和Python语言中的self

      当方法作用于值类型的接收者时,Go语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但是修改操作只是针对副本,无法修改接收者变量本身。

    如:

import "fmt"

// 定义结构体
type User struct {
    name    string
    gender  string
    address string
    age     int
}

// 值类型的接收者
func (user User) setAddress(address string) {
    user.address = address
}

// 指针类型的接收者
func (user *User) setName(name string) {
    user.name = name
}

func main() {
    user := &User{
        name:    "yangyongjie",
        age:     27,
        gender:  "male",
        address: "nanjing",
    }
    
    // 接收值类型的方法,user变量本身值没有被修改
    user.setAddress("beijing")
    fmt.Println(user.address) // nanjing

    // 接收指针类型的方法,user变量本身值没有被修改
    user.setName("yyj")
    fmt.Println(user.name) // yyj

}

 

      什么时候应该使用指针类型接收者?

        ①:需要修改接收者中的值

        ②:接收者是拷贝代价比较大的大对象

        ③:保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者

 

  结构体变量和结构体指针的理解:

    结构体指针指向的是结构体变量的内存地址

    结构体变量是结构体类型的变量本身的值

 

  Go单元测试

import (
	"testing"
)

func TestSendTalos(t *testing.T) {
	
}

 

END.

posted @ 2021-12-23 10:47  杨岂  阅读(1006)  评论(0编辑  收藏  举报