快速入门Go(2)

Go·语法 2019-01-03 9210 字 1413 浏览 点赞

前言

这是我学习Go语法的笔记。由于有C和Python的基础,上手Go很快。笔记很粗糙,好在自己够用。

此篇包括了Go相关的:函数,闭包,类型命名,项目管理等。涉及关键字type defer

函数

无参无返回值函数

func myfun() {
    fmt.Println("this is a func")
}

被调函数放在调用函数的前后都允许,不需要像C那样放在后面需要前置声明。

有参无返回值函数

func myfun(str string) {
    fmt.Println(str)
}

// 形参同类型时
func myfun(str1, str2 string) {
    // ...
}

// 形参不同类型时
func myfun(num int, str string) {
    // ...
}

需要在参数列表中指明参数的类型。与C语言的区别是:形参名在前,类型在后。

不定长参数函数

func myfun(str ...string) {
    // ...
}

在函数类型前加美式省略号(...)可实现不定长参数传参。

func myfun(str ...string) {
    for _, s := range str {
        fmt.Printf("%s\n", s)
    }
}

func main() {
    myfun("hello", "guan", "ying")
}
// 输出
hello
guan
ying

需要注意的是:当参数列表中有多个参数时,不定长参数必须在固定参数的后面。

func myfun(num int, str ...string) {/* ... */}  // 对
func myfun(str ...string, num int) {/* ... */}  // 错

当我们需要把一个函数中的不定长参数传给另一个函数时,也需要用到...

func herfun(num ...int) {
    for _, n := range num {
        fmt.Println(n)
    }
}

func myfun(num ...int) {
    herfun(num...)
    // 如果只需要传最后两个参数,可以
    // herfun(num[1:]...)
}

func main() {
    myfun(1,  2., 3)
}

有返回值函数

一般来说,让人习惯的用法是:

func myfun() int {/* ... */}  // 表示return了一个int类型
func myfun(str string) (byte, byte, byte) {/* ... */}  // 表示return了三个byte类型

此时返回的格式与Python格式相同。如第一个函数可以return 512;第二个函数可以return 'z', 't', 'y'。像下面这样:

func myfun(str string) (byte, byte, byte) {
    return str[0], str[1], str[2]
}

func main() {
    str := "zty"
    z, t, y := myfun(str)
    fmt.Printf("%c, %c, %c", z, t, y)
}

但是,这并不是官方推荐的返回方式。官方更建议一种酷炫、也更一目了然的方法——返回列表中写明返回的变量的名字:

func myfun(str string) (z byte, t byte, y byte) {
    z, t, y = str[0], str[1], str[2]
    return
}

看仔细!在myfun()函数中,我们并没有声明变量z、t、y,但它们确实、已然可用。而return的时候,return就够了。

自然与参数列表一样,当返回值类型相同时我们可以简化代码:

func myfun(str string) (z , t, y byte) {/* ... */}

type

变量类型

关键字type可以为数据类型重命名,让类型有了语义。

func main() {
    type alpha byte  // 类型alpha的底层数据类型是byte
    var z alpha = 'z'
    fmt.Printf("%c 的类型是 %T", z, z)
}
// 输出
z 的类型是 main.alpha

如果我们用alpha来声明一个变量,我们很显然可以知道这个变量企图存放一个字母。这里需要说明一下,Python中获取一个变量的类型通过type()函数;而Go语言是通过格式占位符%T来实现。

通过type命名的数据类型默认有一个方法,可以显式地转换变量的类型:

func main() {
    type positive string
    type negative string

    var pWords positive = "beautiful"
    var nWords negative = "ugly"

    fmt.Printf("%s是%T类型的词语\n", pWords, pWords)
    fmt.Printf("%s是%T类型的词语\n", nWords, nWords)
    // 输出:
    // beautiful是main.positive类型的词语
    // ugly是main.negative类型的词语

    fmt.Printf("%s是%T类型的词语\n", nWords, positive(nWords))  // 显式地转换类型
    // 输出:
    // ugly是main.positive类型的词语
}

这样的转换并非万能,只在允许的情况下使用。比如字符串类型就不能转整数型。所以基于这两个类型之上的类型的变量就不可以相互显式转换:

type (
    str string
    num int
)

var greet str = "hello"
fmt.Printf("%d\n", num(greet))  // 错误的使用方式

报错:cannot convert greet (type str) to type num

函数类型

Go语言也允许对一个函数类型命名,通过这种方式可以实现多态。具体使用方式如下:

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

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

func main() {
    type ComputeFunc func(int, int) int  // 格式:type 类型名 函数类型

    var compfun ComputeFunc
    compfun = add  // 此时compfun()当于add()函数
    resultAdd := compfun(10, 20)
    fmt.Printf("result = %d\n", resultAdd)
    // 输出:result = 30

    compfun = sub  // 此时compfun()相当于sub()函数
    resultSub := compfun(10, 20)
    fmt.Printf("result = %d\n", resultSub)
    // 输出:result = -10
}

匿名函数与闭包

Go语言的匿名函数与JS的相似。不得不说,功能强大。然而Python的匿名函数虽然有些弱鸡,但胜在优雅。我这种外貌协会更偏爱Python风。匿名函数像下面这样:

func main() {
    resutl := func (a int, b int) int {
        return a + b
    } (10, 20)

    fmt.Printf("%d\n", resutl)
}

上述代码的坏处是匿名函数会立即执行,有时候我们期望它能够在我们需要的时候执行——虽然我们早早就定义好了。解决方式是丢弃匿名函数最后的“小尾巴”:

func main() {
    f := func (a int, b int) int { 
        return a + b
    }

    fmt.Printf("我还不想执行1\n")
    fmt.Printf("我还不想执行2\n")
    fmt.Printf("现在想执行了,那就执行吧!\n")
    result := f(12, 21)  // 执行匿名函数
    fmt.Printf("%d\n", result)
}
// 输出:
我还不想执行1
我还不想执行2
现在想执行了,那就执行吧!
33

所以匿名的两种格式:

f := func () int {}  // 不会立即执行
f()  // 此时才执行
// ---------分割线--------------
func () int {} ()  // 立即执行

Python的匿名函数与闭包大多时候是一种概念,只是关键字lambda的存在让其看起来不一样。而Go语言里二者的“外貌”比较统一。区别这个函数是不是闭包的要点在于:这个函数是否使用了与它同一作用域的变量或常量。

func main() {
    alpha := 'z'
    num := 512

    func() {
        alpha = 't'
        num = 1024
    }()

    // alpha 与 num 值已经发生了改变
    fmt.Printf("alpha = %c, num = %d\n", alpha, num)
    // 输出:alpha = t, num = 1024
}

Python处理上面状况的时候存在一些问题。Py2不允许闭包函数直接修改外界变量的值,而Py3中,当你企图修改外界变量时需要用nonlocal关键字说明:

def main():
    alpha = "z"
    num = 512
    def package():
        nonlocal alpha, num  # 对变量进行说明
        alpha = "t"
        num = 1024
    
    package()
    print("alpha = %s, num = %d" % (alpha, num))
    # 输出:alpha = t, num = 1024

main()

闭包的另一个特点是不会被它的作用域限制,只要闭包还在使用,它引用的变量就会一直存在:

func myfun() func() int {
    var x int
    return func() int {
        x++
        return x * x
    }
}

func main() {
    f := myfun()
    fmt.Println(f())  // 输出:1
    fmt.Println(f())  // 输出:4
    fmt.Println(f())  // 输出:9
    // 尽管myfun()的生命周期已经结束
    // 但储存其闭包的内存空间还在
}

defer

常规使用

特点:(1)延迟调用(2)无论程序发生什么错误,总会执行发生错误前的defer修饰的语句(3)多个defer语句的执行顺序是先进后出。

一、延迟调用

package main

import "fmt"

func main() {
    fmt.Println("start ...")
    defer fmt.Println("middle ...")
    fmt.Println("end ...")
}
// 输出:
start ...
end ...  // 注意执行顺序
middle ...

二、无论程序发生什么错误,总会执行发生错误前的defer修饰的语句

func mod(x int) (result int) {
   result = 100 / x
   return
}

func main() {
   defer fmt.Println("start ... ")
   result := mod(0)
   fmt.Println(result)
   defer fmt.Println("end ... ")
}
// 输出:
start ... 
panic: runtime error: integer divide by zero
// 没有执行 `fmt.Println("end ... ")` 语句

三、多个defer语句的执行顺序是先进后出

func main() {
    defer fmt.Println("start ... ")
    defer fmt.Println("end ... ")
}
// 输出:
end ... 
start ... 
// 注意执行顺序

与匿名函数的使用

无传参示例:

func main() {
    a, b := 10, 20
    defer func() {
        fmt.Printf("a = %d, b = %d", a, b)  // 输出:a = 100, b = 200
    } ()
    a, b = 100, 200  // 第二次赋值
}

根据输出值可推断匿名函数是在第二次赋值之后才被调用的。

传参示例:

func main() {
    a, b := 10, 20
    defer func(a int, b int) {
        fmt.Printf("a = %d, b = %d", a, b)  // 输出:a = 10, b = 20
    } (a, b)
    a, b = 100, 200
}

可见,对匿名函数传参时,匿名函数会绑定当时当刻的变量的值(闭包),而不是需要时才获取。这样做可以保护变量不受外界污染。在Python中,想达到此目的会有些麻烦,需要两层嵌套:

def main():
    a, b = 10, 20
    def outer(a, b):
        def inner():
            print("a = %d, b = %d" % (a, b))
        return inner
    
    f = outer(a, b)
    a, b = 100, 200
    f()

main()
# 输出:
a = 10, b = 20

作用域

花括号可以隔离变量:

func main() {
    {
        num := 1024
    }
    fmt.Println(num)
}
// 输出(程序报错):
undefined: num

这跟C语言是如出一辙的。

导包

Go语言中导入包有四种操作。

普通导入

package main
import "fmt"

func main() {
    fmt.Println("this is a test")
}

通过这种方式导包后需要注意两点:第一点,如果需要用到这个包里的变量或函数,需要加上包名,如上面代码所示fmt.;第二点,必须用到fmt包中的变量或者函数至少一次,否则编译器报错。

点操作

package main
import . "fmt"

func main() {
    Println("this is a test")
}

类似Python中的from os import *。此时使用fmt包中的内容需要省略包名。

重命名

package main

import mypen "fmt"

func main() {
    mypen.Println("this is a test")
}

类似Python中的import os as xxx

_下划线操作

package main
import _ "fmt"

func main() {
}

Go语言中要求,导入的包、定义的变量必须被使用,否则不能通过编译器,此时下划线替我们解决了导入包必须被使用的烦恼。如上示代码,即便导入了fmt包,但可以不使用,编译器不会报错。更多时候这样做的目的是为了自动调用导入包的init()函数。

项目管理

实际编程时我们需要将代码写在多个.go的源文件中,这时候需要知道Go语言是如何进行项目管理的(比Python稍稍复杂)。

要求一:代码必须放在src目录下。
要求二:src的父目录必须添加到GOPATH中(可用命令go env进行查看)。
要求三:一个程序必须有一个main包。

同目录下的多个源文件

同目录下的多个源文件要求必须属于同一个包,也就说package xxx中的xxx应该是相同的。

此时在目录D:\MyCode\GoCode\ImportPkg\src下创建两个文件:main.gotest.go。由于一个程序必须有一个main包,而test.gomain.go在同一目录,所以两个文件都应该属于main包。

// main.go
package main

func main() {
    greet()  // 直接使用test.go中的greet()函数
}
// test.go
package main

import "fmt"

func greet()  {
    fmt.Println("hello world.")
}

终端使用命令:go run .go build .编译整个包的文件。

同一个包里的函数可以直接使用。不需要导入,也不要求首字母大写。

不同目录下的多个源文件

不同目录的源文件可以属于不同包。同时,如果需要使用别的目录下的源文件中的函数,必须导入所属包。且使用的函数必须首字母大写。

// main.go
package main

import "demo"

func main() {
    demo.Greet()
}
// demo/test.go
package demo

import "fmt"

func Greet() {
    fmt.Println("hello world.")
}

目录结构如下:

D:\MyCode\GoCode\ImportPkg\src:
                            |——main.go
                            |——demo
                                |——test.go

go install的使用

在src的同级目录中创建bin目录,并添加到系统变量中,此时在src目录下执行命令go install,会在bin目录下生成可执行文件。

看到网上说同时还会生成pkg目录以及该目录下的相关文件,但我的没有,暂时存疑。



本文由 Guan 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

还不快抢沙发

添加新评论