02.函数与包.md

[TOC]

一.函数核心知识点

(一).Go站点

https://go.dev/

(二).不定长参数案例

main.go
#---------------------------------------------
package main

import "fmt"


func calculateAvg(bmis ...float64) float64{
    var total float64
    for _,item := range bmis{
        total+=item
    }
    return total
}

func main(){
    bmis := []float64{1.1,1,2,1.4,9.8}
    //输入的切片数据要进行展开 如果传入的是slice,必须进行值的传入,如果使用不定长参数 可以为0
    fmt.Println(calculateAvg(bmis...))
}
#---------------------------------------------

(三).命名返回值案例

func calculateAvg(bmis ...float64) (total float64){
    //var total float64
    for _,item := range bmis{
        total+=item
    }
    return
}

(四).函数作为参数(回调函数)

- 函数作为参数传入另一个函数,被传入函数有时也称作回调函数。
- 函数变量在赋值时条件:
    - 变量类型不能改变,函数只能作为函数来使用。
    - 变量定义后必须使用,普通函数可以单独存在而不必使用,单函数变量定义后必须使用。
    - 变量类型在赋值过程中,函数的形式参数,返回值必须一致。
- 案例
    #---------------------------------------------
    package main

    import "fmt"

    type callbackFunc func(x,y int) int

    // 提供一个接口 让外部去实现
    func testCallbackFunc(x,y int,callbackFunc2 callbackFunc) int {
        return callbackFunc2(x,y)
    }

    // 回调函数具体实现
    func Add(x,y int) int {
        return x+y
    }
    func main(){
        x,y :=99,199
        fmt.Println(testCallbackFunc(x,y,Add))
    }
    #---------------------------------------------

(五).作用域(经验分享型)

- 作用域是指特定实体的有效范围。
- 作用域中可包含变量/函数/interface/对象等。他们互相可见,可操作。
- 作用域可嵌套、嵌套时,子作用域可见母作用域的所有元素,且子作用域可定义与母作用域同名的变量,常量等,操作时遵循就近原则。
- Golang中使用{}来定义作用域。

案例代码
#---------------------------------------------
package main

import "fmt"

var(
    tall,weight float64
)


func calcAdd() float64 {
    fmt.Println(tall + weight)
    return 0
}

// {}作用域测试函数
func calcBMI(){
    tall,weight = 1.70,70.0
    calculatorAdd := func(a,b float64) float64 {
        return a + b
    }
    result := calculatorAdd(1,3)
    fmt.Println(result)

    // 测试作用域的不同1
    {
        //todo fmt.Scanln...
        personTall :=1.81
        personWeight :=90.0
        result1 := calculatorAdd(personTall,personWeight)
        fmt.Printf("result1:=%v\n",result1)
    }

    // 测试作用域的不同2
    {
        //todo fmt.Scanln...
        personTall :=1.90
        personWeight :=80.0
        result2 := calculatorAdd(personTall,personWeight)
        fmt.Printf("result2:%v\n",result2)
    }
    // fmt.Println(result1) # result1/result2有效范围为花括号内{}
}


func sampleSubdomain(){
    name := "jwgod" // 声明变量 赋值jwgod
    fmt.Println("1.name is :",name)
    {
        fmt.Println("2.name is :",name)

        // 注意这里是=号,重新赋值,所以name="alisa",如果是 := 是声明并初始化 重新分配内存。
        name = "alisa" // 作用域是全局

        // 相当于重新定义了变量,:= 作用域是当前{},执行结束,变量就消失了。
        // name := "alisa"

        fmt.Println("3.name is :",name)
    }

    fmt.Println("4.name is :",name)
    if name == "jwgod"{
        b := "我是变量jwgod结果。"
        fmt.Println(b)
    } else {
        b := "我是变量Alisa结果。"
        fmt.Println(b)

    }
}


func calcBMI2() float64 {
    return 0
}

func sampleSubdomainIf(){

    // v变量生命周期是在当前语句块内
    if v:= calcBMI2();v==0{
        fmt.Println(v)
    }else {
        fmt.Println(v)
    }
    // fmt.Println(v) // v的有效范围为 if block。
}


func main(){
    // RsampleSubdomain()


    // 测试calcAdd()
    fmt.Println("全局变量赋值前。")
    calcAdd()

    tall,weight = 1.70,80
    fmt.Println("全局变量赋值后。")
    calcAdd()

    // 重新定义重名的局部变量
    tall,weight := 1.80,70
    fmt.Println("重新定义后:",tall,weight)
    calcAdd()
    tall,weight= 1.67,60
    calcAdd()
}
#---------------------------------------------

(六).递归

1.若函数调用链中存在自己调用自己,则成这种调用方式为递归。
    func fib(n uint) uint{
        if n == 0 {
            return 0
        }

        if n ==1 {
            return 1
        }

        return fib(n-1) + fib(n-2)
    }

    func  main()  {
        startTime := time.Now()
        fmt.Println(fib(100))
        endTime := time.Now()
        fmt.Println("cost time:",endTime.Sub(startTime))
    }
2.猜数字游戏。
    -1~100中心里想一个数组,然后让程序去猜。
    - 程序员问:是xx么?你只能说高了?低了?对了。
    - 若没猜中,程序继续猜,直到猜中为止。

    #---------------------------------------------
    package main

    import "fmt"

    func guessNumber(left,right uint){
        guess := (left+right)/2
        var getFromInput string
        fmt.Println("我猜是:",guess)

        fmt.Println("高了输入1,低了输入0,对了输入9。")
        fmt.Scanln(&getFromInput)
        switch getFromInput {
        case "1":
            if left == right{
                fmt.Println("你这坏家伙 别闹了~")
                return
            }
            guessNumber(left,guess-1)
        case "0":
            if left == right{
                fmt.Println("你这坏家伙 别闹了~")
                return
            }
            guessNumber(guess+1,right)
        case "9":
            fmt.Printf("你心里想的数字是:%v\n",guess)
        }
    }

    func main()  {
        guessNumber(1,100)
    }
    #---------------------------------------------

(七).闭包

1.一个函数和对其周围状态的引用捆绑在一起,这样的组合称为闭包(closure),闭包让你可以在一个内层函数中访问到
  其外层函数的作用域。

(八).init函数

- 可在同一个文件 同一个包中可以定义多个(特殊的同名函数)
- 如果包里有多个互相引用的package,会先执行最内层的init 函数,最后执行main函数里面的init函数。

(九).defer

1.调用os.Exit()defer不会被执行 案例如下:
    #---------------------------------------------
    package main

    import (
        "fmt"
        "os"
    )

    func openFile(){
        fileName := "/test.txt"
        f,err := os.Open(fileName)

        //defer fmt.Println("test defer.")
        defer func() {
            if info := recover();info != nil{
                fmt.Println(info)
                f.Close()
            }else {
                fmt.Println("这个recover我执行了。")
            }
        }()

        fmt.Println("我是os.Exit执行之前的代码。")
        if err != nil {
            fmt.Println("我是err输出的错误:",err)
            //os.Exit(1)
        }

        fmt.Println("os.Exit执行之后代码。")

        defer f.Close()
        defer func() {
            f.Close()
        }()

    }

    func main()  {
        openFile()
    }
    #---------------------------------------------

(十).panic/recover

1.panicgo内置的函数,用于处理严重错误,使当前函数直接退出,如果异常退出没有捕获,
  则会持续向上层递增,直到有捕获的地方,或者main函数退出。
2.go内置了多种panicnil pointer,index out of range,concurrent read/write、map等。
3.panic也可以主动通过调用panic函数抛出。

    #---------------------------------------------
    package main

    import "fmt"

    func panicAndRecover() {

        // 注意 此处是一个func
        //defer func() {
        //	if info := recover();info != nil{
        //		fmt.Println("错误信息是:",info)
        //	}
        //}()

        // 可以抓取到
        defer panicAndRecoverUpgrade2()
        
        //抓不住严重错误 已经脱离上下文
        //defer func() {
        //	panicAndRecoverUpgrade()
        //}()
        
        var nameScore map[string]int = nil
        nameScore["jwgod"] = 100
        //fmt.Println(nameScore)
    }

    func panicAndRecoverUpgrade() { // 不能获取到recover到信息
        defer func() {
            if info := recover(); info != nil {
                fmt.Println("升级版1错误信息是:", info)
            }
        }()
    }

    func panicAndRecoverUpgrade2() { //可以获取到 defer只运行最外层。
        if info := recover(); info != nil {
            fmt.Println("升级版2错误信息是:", info)
        }
    }

    func main() {
        panicAndRecover()
        fmt.Println("我要开始执行我自己的程序啦。")
    }
    #---------------------------------------------

(十一).代码debug调试(重要)

1.debug.PrintStack()

二.包

(一).包学前预习部分

1.使用Goland时,可能会出现如下错误:
    - package **** is not in GOROOT (***)
    - 解决办法:在当前项目目录下执行go mod init即可解决。
    - 需要开启Goland中/Go Module->Enable Go modules integration

2.比如Goland里面我们使用gin项目
    - 项目工程目录下执行go mod init
    - 项目工程下执行 go get -u github.com/gin-gonic/gin 下载模块
    - 用法 比如调用gin.Default() Goland会自动 import进相关本地下载的模块地址。
    - go mod tidy //增加丢失的模块,去掉未用的模块。
    - go.mod/go.sum是自动管理加载的 无需认为去手动添加。

3.多模块下go生成可执行文件方式
    - cd go工程下面,执行 go build -o name main.go/go build 都可以。
    - 如果不指定 build的文件名将会是和项目名一样。
4.包前使用"_" "github.com/jwgod/sql" // 纯初始化调用
    - 表示包不需要被调用,但是项目需要。
5."_"做为变量时,表示不定义具体对象的变量。
6."_"做为形式参数时,表示一个不存在的形式参数,无法使用。
7.扩展当前包案例:
    - 有时为了简化调用,可以将要引用的包作为当前包的扩展包引用进来,从而可以像使用当前包的函数一样使用引用包的函数。
    - 注意事项:
        - 同一个包中函数名称,变量名称不能重复。
        - 扩展包中的函数不可控。

    #---------------------------------------------
    package main

    import (
        "fmt"
        . "fmt"
    )

    func extendCurrentPackage(){
        fmt.Println("我打印的是fmt.Println()")
        Println("我打印的是扩展当前包中的println()")
    }

    func main()  {
        extendCurrentPackage()
    }
    #---------------------------------------------
    
8.包别名
    - 有时候引用包的时候,引用的多个包的名称会相同,这时需要使用别名来进行区分。
    - 有时候也通过别名来简化引用。

    #---------------------------------------------
    package main

    import (
        c02 "github.com/jwgod/lesson1/tcpping"
        c03 "github.com/jwgod/lesson2/tcpping"
    )

    func main()  {
        c02.Addr("0.0.0.0")
        c03.Addr("0.0.0.0")
    }
    #---------------------------------------------

(二).包正式课堂部分

1.包是在同一个目录或者文件夹下的,总是一起编译的一组源文件的集合。
2.在同一个包中:
    - 所有函数、变量、常量、对象等都是可见的(visible)3.在不同包中:
    - 只有公开的(public)函数、变量、常量、对象等都是课件的(visible)4.根据函数的是否可访问,分两种:
    - 公有性(public),可以被其他package访问的函数、变量。
        - 以大写字母开头的函数是共有函数、共有变量。
    - 私有(private),只能在当前package访问的函数、变量。
        - 不是以大写字母开头的函数均为私有函数,私有变量。

(三).Go Moudle实战-包引用

[课程章节注释]:(第七次课笔记-20211216 18:00–22:00)

1.体脂计算器接受从命令行传入的姓名、性别、身高、体重、年龄、直接计算出其体脂并给出结果。

2.引入命令行使用工具包方式
    - _ "github.com/spf13/cobra" // 第三方输入包
    - go mod tidy // 初始化引用下载包
    - 按住command键盘,鼠标点一下cobra,会自动跳转到包目录位置。
3. go mod init 说明:
    - 如果在GOPATH下 可以直接指向go mod init
    - 如果项目不在GOPATH下,需要指定路径: go mod init example/module,执行代码 需要go run ./main.go 指定下,否则可能会报错。
4.Go Module替换
    - Go Module 替换(replace)是用另一个实现替换默认要使用的实现。类似"狸猫换太子"。

    module lesson2/13.read

    go 1.17

    require (
        github.com/spf13/cobra v1.3.0
        learn.go.tools v0.0.0-00010101000000-000000000000
    )

    require (
        github.com/inconshreveable/mousetrap v1.0.0 // indirect
        github.com/spf13/pflag v1.0.5 // indirect
    )

    // 这里是拿一个不存在的为案例做替换 注意 要替换的包里面要有go.mod
    replace learn.go.tools => ../../learn.go.tools

    - 编辑完毕后执行 go mod tidy即可
        - [jwgod@infra 13.read %]# go mod tidy
        - go: found learn.go.tools in learn.go.tools v0.0.0-00010101000000-000000000000

5.项目核心代码

    package main

    import (
        "fmt"
        "github.com/spf13/cobra" // 第三方输入包
        "lesson2/13.read/sex"
        "lesson2/13.read/bmi"
        "lesson2/13.read/bfr"
        "learn.go.tools"
    )

    var (
        Name   string
        Sex    string
        Tall   float64 //string
        Weight float64 //string
        Age    float64 //string
    )

    // 最原始的操作方式 程序不健壮传参方式
    //func ArgsExample() {
    //	arguments := os.Args
    //	Name = arguments[1]
    //	Sex = arguments[2]
    //	Tall = arguments[3]
    //	Weight = arguments[4]
    //	Age = arguments[5]
    //
    //	fmt.Println(arguments)
    //	fmt.Println("执行脚本是:", arguments[0])
    //	fmt.Println("Name:", Name)
    //	fmt.Println("Sex:", Sex)
    //	fmt.Println("Tall:", Tall)
    //	fmt.Println("Weight:", Weight)
    //	fmt.Println("Age:", Age)
    //}

    func InputFromCobra(){
        cmd := cobra.Command{
            Use: "healthcheck", //如何使用
            Short: "体脂计算器,根据身高,体重,性别,年龄计算体制比,给出建议。", // 短描述
            Long: "该体脂计算器是基于bmi的体脂计算器进行计算的...", // 大段的描述
            Run: func(cmd *cobra.Command, args []string) {
                fmt.Println("Name:", Name)
                fmt.Println("Sex:", Sex)
                fmt.Println("Tall:", Tall)
                fmt.Println("Weight:", Weight)
                fmt.Println("Age:", Age)

                // 计算
                bmiResult := bmi.GetBMI(Tall,Weight)
                bfrResult := bfr.GetBFR(bmiResult,Age,sex.GetSexWeight(Sex))
                fmt.Printf("bmi:%v,bfr:%v\n",bmiResult,bfrResult)
                fmt.Println(learn_go_tools.Max(3,100))
                // 评估结果
            },
        }

        cmd.Flags().StringVar(&Name,"Name","","姓名")
        cmd.Flags().StringVar(&Sex,"Sex","","姓别")
        cmd.Flags().Float64Var(&Tall,"Tall",-1,"身高")
        cmd.Flags().Float64Var(&Weight,"Weight",-1,"体重")
        cmd.Flags().Float64Var(&Age,"Age",-1,"年龄")

        // 运行命令行对象
        cmd.Execute()
    }

    func main() {
        //ArgsExample()
        InputFromCobra()
    }

6.go.mod replace
    - go.mod 文件中 包版本可以写 master or v1.2.1 这两种类型。
    - replace learn.go.tools => ../../learn.go.tools master

(四).Vendor

1.Vendor是把项目module定义的所有的依赖制作一个副本保存在项目的vendor目录。 
    -:将项目定义的依赖做一个快照并保存下来,避免项目依赖的变更影响项目的一致性。
    - go mod vendor -v // 将项目依赖保存到vendor中。

2.Go Module 与 vendor 共存
    - Go build/run 通过命令行选项来控制 -mod=<vendor|mod|readonly>。
        • vendor:使用 vendor 中的依赖编译项目。
        • mod:使用 go module 定义的依赖编译项目,并且会自动更新 go.mod 定义。
        • readonly:使用 go module 定义的依赖编译项目,并且不做任何依赖的升级。
    - 注意:
        • 项目中如果有vendor目录,Go在编译时默认使用vendor提供的依赖。 
        • 如果没有vendor目录,Go在编译时默认使用readonly。

三.Golang内置函数

1.close
2.len,cap
    - capacity
    - 当数据初始化容量后 随着容量的递增,会不断的重新分配空间。
    - 在空间扩展的过程中 会不断进行原有数据复制 到新的内存区域的情况。
    - 考虑高性能的情况下 建议直接定义好 cap容量,避免数据在内存中频繁复制。

3.new,make  // 操作内存

    func makeTest(){

        // 未进行初始化之前 长度和容量都为0
        arr2 := []int{}
        var arr3 []int
        fmt.Printf("len=%d,cap=%d\n",len(arr2),cap(arr2))
        fmt.Printf("len=%d,cap=%d\n",len(arr3),cap(arr3))

        // 如果长度为0 容量即使大于0 也会出现out of range错误。
        //arr4 := make([]int)

        // 这里可以使用append进行初始化操作并自动扩容
        var arr4 []int
        fmt.Printf("len=%d,cap=%d\n",len(arr4),cap(arr4))
        for i :=0;i<4;i++{
            arr4 = append(arr4,i)
        }
        fmt.Println(arr4)
    }

    func newTest(){
        {
            // new后变量被定义为一个指针类型 分配到了一块内存地址 但是并未进行赋值
            i := new(int)
            fmt.Println(reflect.TypeOf(i))
            fmt.Println(&i)
        }

        i := 5
        fmt.Println(&i)
        var j *int //此时为空指针 默认为nil
        fmt.Println(j)
        j =&i
        fmt.Println(j)
    }

    func copyTest(){

        // 匿名函数 计算代码执行的时长 us微秒
        // defer 入栈 先进后出 声明之处值已确定 中间不会动态改变
        startTime := time.Now()
        defer func(){
            endTime := time.Now()
            fmt.Println(startTime,endTime)
            fmt.Println(endTime.Sub(startTime))
        }()

        //copy总是以最小长度为基准
        i := make([]int,4,10)
        i1 := make([]int,5,11)
        copy(i,i1)
        fmt.Println(i)
}

4.copy,append   // 操作切片
5.panic,recover

    - 自己总结学习版本
    #---------------------------------------------
    package main

    import "fmt"

    func testPanicRecovery(){

        // 入栈延迟函数
        str1 := "我是str1变量,这里是recovery的开始。"
        defer func(){
            fmt.Println(str1)
            if info := recover();info != nil{
                fmt.Println(info) // 这里的info其实就是panic传入的内容
            }else {
                fmt.Println("程序正常退出。")
            }

        }()
        // 首次执行函数
        fmt.Println("我是defer和panic之间的输出。")

        //panic之后的代码不再执行
        panic("我是panic,手动引发一次宕机事件。")

        str2 := "我是str2变量。"
        defer func(){
            fmt.Println(str2)
        }()
    }

    func main(){
        testPanicRecovery()
    }
    #---------------------------------------------

6.print,println
7.complex,real,imag // 操作复数

四.函数方法论

1.写整洁代码的原因
    - 更容易看懂
    - 更容易修改
    - 更容易测试
    - 更容易上线

2.怎么写整洁的方法、函数
    - 主要逻辑清晰

    - 避免冗长的方法、函数(控制在100行以内)
        - 将代码拆分为子方法
        - 提取出特定的对象,将各个方法转化为特定对象的方法。

    - 避免复杂的逻辑嵌套
        - 嵌套内部的逻辑拆分为子方法。

    - 目的单一
        - 方法的目的要单一 一次只做一件事。事情可大,可小。
    
    - 要起一个好名字

    - 避免太多的方法参数、返回值
        - 函数按功能拆分
        - 定义特定的对象、成员函数。

    - 一致的方法抽象层
        - 在调用方法、逻辑计算时,方法的抽象层要一致。

    - 读写分离
        - 对变量、成员变量的读取、写入要分离。避免在同一个方法中同时操作读取属性的一类操作或者写入类型的一类操作。