Golang开发_函数、工程管理


JetBrains的IDE出的这个新UI挺好看的

01_Golang开发_函数、工程管理

函数

定义格式

  • 函数构成代码执行的逻辑结构。
  • 在 Go 语言中,函数的基本组成为:关键字 func、函数名、 参数列表、返回值、函数体和返回语句

自定义函数

无参无返回值

package main

import "fmt"

func Test() { //无参无返回值函数定义
    fmt.Println("this is a test func")
}
func main() {
    Test() //无参无返回值函数调用
}

有参无返回值

普通参数列表

func Test1(v1 int, v2 int) { //方式 1
    fmt.Println("v1 = %d, v2 = %d\n", v1, v2)
}

func Test2(v1, v2 int) { //方式 2, v1, v2 都是 int 类型
    fmt.Println("v1 = %d, v2 = %d\n", v1, v2)
}

func main() {
    //函数调用
    Test1(10, 20)
    Test2(18, 20)
}

不定参数列表

不定参数类型
  • 不定参数是指函数传入的参数个数为不定数量。
  • 为了做到这点,首先需要将函数定义为接受 不定参数类型(...type)
// 不定参数是指函数传入的参数个数为不定数量。为了做到这点,首先需要将函数定义为接受
// 不定参数类型:
// 形如...type 格式的类型只能作为函数的参数类型存在,并且必须是最后一个参数
func Test3(args ...int) {
    for _, n := range args {
        fmt.Println(n)
    }
}

func main() {
    //函数调用
    Test3()
    Test3(1)
    Test3(1, 9, 9)
}
不定参数的传递
// 不定参数的传递
func MyFunc01(args ...int) {
    fmt.Println("MyFunc01")
    for _, n := range args { //遍历参数列表
        fmt.Println(n)
    }
}
func MyFunc02(args ...int) {
    fmt.Println("MyFunc02")
    for _, n := range args { //遍历参数列表
        fmt.Println(n)
    }
}

func Test4(args ...int) {
    MyFunc01(args...)     //按原样传递, Test()的参数原封不动传递给 MyFunc01
    MyFunc02(args[1:]...) //Test()参数列表中,第 1 个参数及以后的参数传递给 MyFunc02
}
func main() {
    Test4(1, 2, 3) //函数调用
}

无参有返回值

  • 有返回值的函数,必须有明确的终止语句,否则会引发编译错误。

一个返回值

  • 最好命名返回值,因为不命名返回值,虽然使得代码更加简洁了,但是会造成生成的文档可读性差。
// 一个返回值
func Test5() int { //方式 1
    return 250
}

// 官方建议:最好命名返回值,因为不命名返回值,虽然使得代码更加简洁了,但是会造成生 成的文档可读性差
func Test6() (value int) { //方式 2, 给返回值命名
    value = 250
    return value
}

func Test7() (value int) { //方式 3, 给返回值命名
    value = 250
    return
}

func main() {
    v1 := Test5() //函数调用
    v2 := Test6() //函数调用
    v3 := Test7() //函数调用
    fmt.Printf("v1 = %d, v2 = %d, v3 = %d\n", v1, v2, v3)
}

多个返回值

// 多个返回值
// 方式 1
func Test9() (int, string) {
    return 260, "hello"
}

// 方式 2, 给返回值命名
func Test10() (a int, str string) {
    a = 260
    str = "holy"
    return
}

func main() {
    v1, v2 := Test9() //函数调用
    _, v3 := Test10() //函数调用, 第一个返回值丢弃
    v4, _ := Test10() //函数调用, 第二个返回值丢弃
    fmt.Printf("v1 = %d, v2 = %s, v3 = %s, v4 = %d\n", v1, v2, v3, v4)
}

有参有返回值

例:求2个数的最小值和最大值

// 有参有返回值
// 求 2 个数的最小值和最大值
func MinAndMax(num1 int, num2 int) (min int, max int) {
    if num1 > num2 {
        min = num2
        max = num1
    } else {
        max = num2
        min = num1
    }
    return
}

func main() {
    min, max := MinAndMax(33, 22)
    fmt.Printf("min = %d, max = %d\n", min, max) //min = 22, max = 33
}

递归函数

  • 递归指函数可以直接或间接的调用自身。
  • 递归函数通常有相同的结构:一个跳出条件和一个递归体
  • 所谓跳出条件就是根据传入的参数判断是否需要停止递归,而递归体则是函数自身所做的一些处理。

例:计算1+2+3……+100

package main

import "fmt"

// 通过循环实现 1+2+3……+100
func Test01() int {
    i := 1
    sum := 0
    for i = 1; i <= 100; i++ {
        sum += i
    }
    return sum
}

// 通过递归实现 1+2+3……+100
func Test02(num int) int {
    if num == 1 {
        return 1
    }
    return num + Test02(num-1) //函数调用本身
}

// 通过递归实现 1+2+3……+100
func Test03(num int) int {
    if num == 100 {
        return 100
    }
    return num + Test03(num+1) //函数调用本身
}

func main() {
    fmt.Println(Test01())    //5050
    fmt.Println(Test02(100)) //5050
    fmt.Println(Test03(1))   //5050
}

函数类型

  • 在 Go 语言中,函数也是一种数据类型,我们可以通过 type 来定义它
  • 它的类型就是所有拥有相同的参数,相同的返回值的一种类型。
package main

import "fmt"

type FuncType func(int, int) int //声明一个函数类型, func 后面没有函数名

// 函数中有一个参数类型为函数类型:f FuncType
func Calc(a, b int, f FuncType) (result int) {
    result = f(a, b) //通过调用 f()实现任务
    return
}

func Add(a, b int) int {
    return a + b
}

func Minus(a, b int) int {
    return a - b
}

func main() {
    //函数调用,第三个参数为函数名字,此函数的参数,返回值必须和 FuncType 类型一致
    result := Calc(1, 1, Add)
    fmt.Println(result) //2
    var f FuncType = Minus
    fmt.Println("result = ", f(10, 2)) //result = 8
}

匿名函数与闭包

  • 所谓闭包就是一个函数“捕获”了和它在同一作用域的其它常量和变量。
  • 这就意味着当闭包被调用的时候,不管在程序什么地方调用,闭包能够使用这些常量或者变量。
  • 它不关心这捕获了的变量和常量是否已经超出了作用域,所以只有闭包还在使用它,这些变量就还会存在。
  • 在 Go 语言里,所有的匿名函数(Go 语言规范中称之为函数字面量)都是闭包。
  • 匿名函数是指不需要定义函数名的一种函数实现方式
package main

import "fmt"

func main() {
    i := 11
    str := "mike"

    f1 := func() { //匿名函数,无参无返回值
        //引用到函数外的变量
        fmt.Printf("方式 1:i = %d, str = %s\n", i, str)
    }

    f1() //函数调用

    //-------------------------
    //方式 1 的另一种方式
    type FuncType func() //声明函数类型, 无参无返回值
    var f2 FuncType = f1
    f2() //函数调用

    //-------------------------
    //方式 2
    var f3 FuncType = func() {
        fmt.Printf("方式 2:i = %d, str = %s\n", i, str)
    }
    f3() //函数调用

    //-------------------------
    //方式 3 ,直接调用此匿名函数
    func() {
        fmt.Printf("方式 3:i = %d, str = %s\n", i, str)

    }() //别忘了后面的(), ()的作用是,此处直接调用此匿名函数

    //-------------------------
    //方式 4, 匿名函数,有参有返回值
    v := func(a, b int) (result int) {
        result = a + b
        return
    }(1, 1) //别忘了后面的(1, 1), (1, 1)的作用是,此处直接调用此匿名函数, 并传参
    fmt.Println("v = ", v)
}

闭包捕获外部变量特点

  • 闭包以引用方式捕获外部变量
  • 内部改了,外部也同样修改
    //闭包捕获外部变量特点
    func() {
        i = 100
        str = "go"
        //内部:i = 100, str = go
        fmt.Printf("内部:i = %d, str = %s\n", i, str)
    }() //别忘了后面的(), ()的作用是,此处直接调用此匿名函数
    //外部:i = 100, str = go
    fmt.Printf("外部:i = %d, str = %s\n", i, str)

函数返回值为匿名函数

  • 函数 squares 返回另一个类型为 func() int 的函数。对 squares 的一次调用会生成一个局部变量 x 并返回一个匿名函数。每次调用时匿名函数时,该函数都会先使 x 的值加 1,再返回 x 的平方。
  • 第二次调用 squares 时,会生成第二个 x 变量,并返回一个新的匿名函数。新匿名 函数操作的是第二个 x 变量。
  • 变量的生命周期不由它的作用域决定squares 返回后,变量 x 仍 然隐式的存在于 f 中。
// squares 返回一个匿名函数,func() int
// 该匿名函数每次被调用时都会返回下一个数的平方
func squares() func() int {
    var x int
    return func() int {
        x++
        return x * x
    }
}

func main() {
    f := squares()
    fmt.Println(f()) // "1"
    fmt.Println(f()) // "4"
    fmt.Println(f()) // "9"
    fmt.Println(f()) // "16"
}

延迟调用defer

defer作用

  • 关键字 defer ⽤于延迟一个函数或者方法(或者当前所创建的匿名函数)的执行。
  • 注意,defer 语句只能出现在函数或方法的内部
  • defer 语句经常被用于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁。
  • 通过 defer 机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放。
  • 释放资源的 defer 应该直接跟在请求资源的语句后。
package main

import "fmt"

func main() {
    fmt.Println("hr")
    defer fmt.Println("dd") //main 结束前调用

    //运行结果:
    //hr
    //dd
}

多个defer执行顺序

  • 如果一个函数中有多个 defer 语句,它们会以 LIFO(后进先出)的顺序执行。
  • 哪怕函数或某个延迟调用发生错误,这些调用依旧会被执⾏。
func test(x int) {
    fmt.Println(100 / x) //x 为 0 时,产生异常
}

func main() {
    defer fmt.Println("hr")
    defer fmt.Println("dd") //main 结束前调用

    defer test(0)

    defer fmt.Println("end")

    //运行结果:
    //    end
    //    dd
    //    hr
    //panic: runtime error: integer divide by zero
}

defer和匿名函数结合使用

// defer和匿名函数结合使用
func main() {
    a, b := 10, 20
    defer func(x int) {       // a 以值传递方式传给 x
        fmt.Println("defer:", x, b)        // b 闭包引用
    }(a)

    a += 100
    b += 1000
    fmt.Println(a, b)

    //运行结果:
    //110 1020
    //defer: 10 1020
}

获取命令行参数

package main

import (
    "fmt"
    "os"
)

func main() {
    args := os.Args //获取用户输入的所有参数

    //如果用户没有输入,或参数个数不够,则调用该函数提示用户
    if args == nil || len(args) < 2 {
        fmt.Println("err: xxx ip port")
        return
    }
    ip := args[1]   //获取输入的第一个参数
    port := args[2] //获取输入的第二个参数
    fmt.Printf("ip = %s, port = %s\n", ip, port)
}

作用域

  • 作用域为已声明标识符所表示的常量、类型、变量、函数或包在源代码中的作用范围。

局部变量

  • 在函数体内声明的变量、参数和返回值变量就是局部变量,它们的作用域只在函数体内
package main

import "fmt"

func test0(a, b int) {
    var c int
    a, b, c = 1, 2, 3
    fmt.Println(a, b, c)
}

func main() {
    //a, b, c = 1, 2, 3 //err, a, b, c 不属于此作用域
    {
        var i int
        i = 10
        fmt.Println("i=", i)
    }
    //i = 20 //err, i 不属于此作用域

    if a := 3; a == 3 {
        fmt.Println("a=", a)
    }
    //a = 4 //err,a 只能 if 内部使用
}

全局变量

  • 在函数体外声明的变量称之为全局变量,全局变量可以在整个包甚至外部包(被导出后)使用。
// 全局变量
var a int //全局变量的声明

func test00() {
    fmt.Println("test00=", a)
}

func main() {
    a = 10
    fmt.Println("a=", a)
    test00()
    //a= 10
    //test00= 10
}

不同作用域同名变量

  • 在不同作用域可以声明同名的变量
  • 其访问原则为:在同一个作用域内,就近原则访问最近的变量;如果此作用域没有此变量声明,则访问全局变量,如果全局变量也没有,则报错
// 不同作用域同名变量
var a int //全局变量的声明

func test01(a float32) {
    fmt.Printf("a type = %T\n", a) //a type = float32
}

func main() {
    fmt.Printf("a type = %T\n", a) //a type = int,说明使用全局变量的 a

    var a uint8
    {
        var a float64
        fmt.Printf("a type = %T\n", a) //a type = float64
    }
    fmt.Printf("a type = %T\n", a) //a type = uint8

    test01(3.14)
    test02()
}

func test02() {
    fmt.Printf("a type = %T\n", a) //a type = int,说明使用全局变量的 a

}

工程管理

  • 在工程中不会简单到只有一个源代码文件,且源文件之间会有相互的依赖关系。如果这样一个文件一个文件逐步编译,那不亚于一场灾难。
  • 早期 Go 语言使用 makefile 作为临时方案,到了 Go 1 发布时引入了强大无比的 Go 命令行工 具。
  • Go 命令行工具的革命性之处在于彻底消除了工程文件的概念,完全用目录结构和包名来推导工程结构和构建顺序。
  • 针对只有一个源文件的情况讨论工程管理看起来会比较多余,因为这可以直接用 go rungo build 搞定。

工作区

  • Go 代码必须放在工作区中。
  • 工作区其实就是一个对应于特定工程的目录,它应包含 3 个子目录:src 目录、pkg 目录和 bin 目录。

  • 目录 src 用于包含所有的源代码,是 Go 命令行工具一个强制的规则,而 pkg 和 bin 则无需手动创建。
  • 如果必要 Go 命令行工具在构建过程中会自动创建这些目录。
  • 需要特别注意的是,只有当环境变量 GOPATH 中只包含一个工作区的目录路径时,go install 命令才会把命令源码安装到当前工作区的 bin 目录下。
  • 若环境变量 GOPATH 中包含多个工 作区的目录路径,像这样执行 go install 命令就会失效,此时必须设置环境变量 GOBIN。

GOPATH设置

  • 为了能够构建这个工程,需要先把所需工程的根目录加入到环境变量 GOPATH 中。
  • 否则, 即使处于同一工作目录(工作区),代码之间也无法通过绝对代码包路径完成调用。
  • 在实际开发环境中,工作目录往往有多个,这些工作目录的目录路径都需要添加至 GOPATH
  • 当有多个目录时,请注意分隔符,多个目录的时候 Windows 是分号,Linux 系统是冒号,当有多个 GOPATH 时,默认会将 go get 的内容放在第一个目录下。

  • 所有 Go 语言的程序都会组织成若干组文件,每组文件被称为一个包。
  • 这样每个包的代码都可以作为很小的复用单元,被其他项目引用。
  • 一个包的源代码保存在一个或多个以.go为文件后缀名的源文件中,通常一个包所在目录路径的后缀是包的导入路径。

自定义包

  • 对于一个较大的应用程序,我们应该将它的功能性分隔成逻辑的单元,分别在不同的包里实现。
  • 我们创建的的自定义包最好放在 GOPATH 的 src 目录下(或者 GOPATH src 的某个子 目录)。
  • 在 Go 语言中,代码包中的源码文件名可以是任意的。
  • 但是,这些任意名称的源码文件都必须以包声明语句作为文件中的第一行,每个包都对应一个独立的名字空间
package calc
  • 包中成员以名称⾸字母⼤⼩写决定访问权限:(注意:同一个目录下不能定义不同的 package

main包

  • 在 Go 语言里,命名为 main 的包具有特殊的含义。
  • Go 语言的编译程序会试图把这种名字的包编译为二进制可执行文件。
  • 所有用 Go 语言编译的可执行程序都必须有一个名叫 main 的包,一个可执行程序有且仅有一个 main 包。
  • 当编译器发现某个包的名字为 main 时,它一定也会发现名为main()的函数,否则不会创 建可执行文件。
  • main()函数是程序的入口,所以,如果没有这个函数,程序就没有办法开始执行。
  • 程序编译时,会使用声明 main 包的代码所在的目录的目录名作为二进制可执行文件的文件名。

init 函数和 main 函数

main.go

// // main.go
package main

import (
    "fmt"
    "test"
)

func main() {
    fmt.Println("main.go main() is called")
    test.Test()
}

test.go

// test.go
package test

import "fmt"

func init() {
    fmt.Println("test.go init() is called")
}

func Test() {
    fmt.Println("test.go Test() is called")
}

导入包

  • 导入包需要使用关键字 import,它会告诉编译器你想引用该位置的包内的代码。
  • 包的路径可以是相对路径,也可以是绝对路径。
//方法 1
import "calc"
import "fmt"

//方法 2
import (
    "calc"
    "fmt"
)
  • 标准库中的包会在安装 Go 的位置找到。
  • Go 开发者创建的包会在 GOPATH 环境变量指定的目录里查找。
  • GOPATH 指定的这些目录就是开发者的个人工作空间。
  • 如果编译器查遍 GOPATH 也没有找到要导入的包,那么在试图对程序执行 run 或者 build 的时候就会出错。

点操作

import (
    //这个点操作的含义是这个包导入之后在你调用这个包的函数时,可以省略前缀的包名
    . "fmt"
)

func main() {
    Println("hello go")
}

别名操作

  • 在导⼊时,可指定包成员访问⽅式,⽐如对包重命名,以避免同名冲突
import (
    io "fmt" //fmt 改为为 io
)

func main() {
    io.Println("hello go") //通过 io 别名调用
}

_操作(忽略此包)

  • 有时,用户可能需要导入一个包,但是不需要引用这个包的标识符。
  • 在这种情况,可以使用 空白标识符_来重命名这个导入
  • _操作其实是引入该包,而不直接使用包里面的函数,而是调用了该包里面的 init 函数。
import (
    _ "fmt"
)

GOPATH设置

go env 查看GOPATH路径

测试案例

main.go

package main

import (
    "calc"
    "fmt"
)

func main() {
    a := calc.Add(1, 2)
    fmt.Println("a = ", a)
}

calc.go

package calc

func Add(a, b int) int { //加
    return a + b
}
func Minus(a, b int) int { //减
    return a - b
}
func Multiply(a, b int) int { //乘
    return a * b
}
func Divide(a, b int) int { //除
    return a / b
}

go install

  • 自动生成bin或pkg目录,需要使用go install命令(了解)
  • 除了要配置GOPATH环境变量,还要配置GOBIN环境变量。

声明:三二一的一的二|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - Golang开发_函数、工程管理


三二一的一的二