GO的第三天,包和做用域

今天的内容看起来简单,其实深刻了解的话仍是蛮难的!不过,咱们也是GO的初学者的一个身份,咱们先从简单的理解开始吧。模块化

什么是包?Go语言中的包和其余语言的库或模块的概念相似,目的都是为了支持模块化、封装、单独编译和代码重用。函数

import "fmt"

上面的代码中,fmt 就是一个包,是经过关键词import导入的。那么,咱们怎么制做本身的包呢?一个包的源代码保存在 一个或多个 以.go为文件后缀名的源文件中,一般一个包所在 目录路径 的后缀是包的导入路径。每一个源文件都是以包的 声明语句 开始,用来指明 包的名字 。若是你写了一个本身的包,使用 相对路径 来引用,那么这个包通常放在GO的工做目录下的 src 文件夹下,才能够被引入使用。工具

每一个包都对应一个独立的名字空间。例如,在image包中的Decode函数和在unicode/utf16包中的 Decode函数是不一样的。要在外部引用该函数,必须显式使用image.Decode或utf16.Decode形式访问。日志

包还可让咱们经过控制哪些名字是外部可见的来隐藏内部实现信息。在Go语言中,一个简单的规则是:若是一个名字是大写字母开头的,那么该名字是能够导出的。code

在每一个源文件的包声明前紧跟着的注释是包注释。一般,包注释的第一句应该先是包的功能概要说明。一个包一般只有一个源文件有包注释(若是有多个包注释,目前的文档工具会根据源文件名的前后顺序将它们连接为一个包注释)。若是包注释很大,一般会放到一个独立的 doc.go 文件中。排序

导入包

在Go语言程序中,每一个包都有一个全局惟一的导入路径。Go语言的规范并无定义这些字符串的具体含义或包来自哪里,它们是由 构建工具 来解释的。当使用Go语言自带的go工具箱时,一个导入路径表明一个目录中的一个或多个Go源文件。生命周期

除了包的导入路径,每一个包还有一个包名,包名通常是短小的名字(并不要求包名是惟一的),包名在包的声明处指定。 按照惯例,一个包的名字和包的导入路径的最后一个字段相同。作用域

若是导入了一个包,可是又没有使用该包将被看成一个编译错误处理。unicode

包的初始化

包的初始化首先是解决包级变量的依赖顺序,而后按照包级变量声明出现的顺序依次初始化:文档

var a = b + c // a 第三个初始化, 为 3
var b = f()   // b 第二个初始化, 为 2, 经过调用 f (依赖c)
var c = 1     // c 第一个初始化, 为 1

func f() int { return c + 1 }

若是包中含有多个.go源文件,它们将按照发给编译器的顺序进行初始化,Go语言的构建工具首先会将.go文件根据文件名排序,而后依次调用编译器编译。

对于在包级别声明的变量,若是有初始化表达式则用表达式初始化,还有一些没有初始化表达式的,例如某些表格数据初始化并非一个简单的赋值过程。在这种状况下,咱们能够用一个特殊的 init初始化函数 来简化初始化工做。每一个文件都 能够包含多个init初始化函数

func init() { /* ... */ }

这样的init初始化函数除了 不能被调用或引用外,其余行为和普通函数相似。在每一个文件中的init初始化函数,在程序开始执行时按照它们声明的顺序被自动调用。

每一个包在解决依赖的前提下,以导入声明的顺序初始化,每一个包只会被初始化一次。所以,若是一个p包导入了q包,那么在p包初始化的时候能够认为q包必然已经初始化过了。初始化工做是自下而上进行的,main包最后被初始化。以这种方式,能够确保在main函数执行以前,全部依赖的包都已经完成初始化工做了。

做用域

做用域咱们能够套用PHP的做用域概念来理解,简单归纳就是 函数内部声明的变量不能被外部使用,内部声明屏蔽了外部同名的声明

不要将做用域和生命周期混为一谈。声明语句的 做用域 对应的是 一个源代码 的文本区域;它是一个编译时的 属性。一个变量的生命周期是指程序运行时变量存在的 有效时间段,在此时间区域内它能够被程序的其余部分引用;是一个 运行时的概念

声明语句对应的词法域决定了做用域范围的大小。对于内置的类型、函数和常量,好比int、len和true等是在 全局做用域 的,所以能够在整个程序中直接使用。任何在在函数外部(也就是包级语法域)声明的名字能够在 同一个包 的任何源文件中访问的。

一个程序可能包含多个同名的声明,只要它们在不一样的词法域就没有关系。例如,你能够声明一个局部变量,和包级的变量同名。可是物极必反,若是滥用不一样词法域可重名的特性的话,可能致使程序很难阅读。

当编译器遇到一个名字引用时,若是它看起来像一个声明,它首先从最内层的词法域向全局的做用域查找。若是查找失败,则报告“未声明的名字”这样的错误。若是该名字在内部和外部的块分别声明过,则内部块的声明首先被找到。在这种状况下,内部声明屏蔽了外部同名的声明,让外部的声明的名字没法被访问

隐式和显示

下面的例子一样有三个不一样的x变量,每一个声明在不一样的词法域,一个在函数体词法域,一个在for隐式的初始化词法域,一个在for循环体词法域;只有两个块是显式建立的:

func main() {
    x := "hello"
    for _, x := range x {   // 隐式
        x := x + 'A' - 'a'
        fmt.Printf("%c", x) // "HELLO" (one letter per iteration)
    }
}

在这个程序中:

if f, err := os.Open(fname); err != nil {
    return err
}
f.ReadByte() // compile error: undefined f
f.Close()    // compile error: undefined f

变量f的做用域只有在if语句内,所以后面的语句将没法引入它,这将致使编译错误。你可能会收到一个局部变量f没有声明的错误提示,具体错误信息依赖编译器的实现。

一般须要在if以前声明变量,这样能够确保后面的语句依然能够访问变量:

f, err := os.Open(fname)
if err != nil {
    return err
}
f.ReadByte()
f.Close()

短变量语句做用域

要特别注意短变量声明语句的做用域范围,考虑下面的程序,它的目的是获取当前的工做目录而后保存到一个包级的变量中。这能够原本经过直接调用os.Getwd完成,可是将这个从主逻辑中分离出来可能会更好,特别是在须要处理错误的时候。函数log.Fatalf用于打印日志信息,而后调用os.Exit(1)终止程序。

var cwd string

func init() {
    cwd, err := os.Getwd() // compile error: unused: cwd
    if err != nil {
        log.Fatalf("os.Getwd failed: %v", err)
    }
}

虽然cwd在外部已经声明过,可是:=语句仍是将cwd和err 从新声明 为新的局部变量。由于内部声明的cwd将屏蔽外部的声明,所以上面的代码并不会正确更新包级声明的cwd变量。

因为当前的编译器会检测到局部声明的cwd并无本使用,而后报告这多是一个错误,可是这种检测并不可靠。由于一些小的代码变动,例如增长一个局部cwd的打印语句,就可能致使这种检测失效。

有许多方式能够避免出现相似潜在的问题。最直接的方法是经过单独声明err变量,来避免使用:=的简短声明方式:

var cwd string

func init() {
    var err error
    cwd, err = os.Getwd()
    if err != nil {
        log.Fatalf("os.Getwd failed: %v", err)
    }
}

参考

《GO语言圣经》

相关文章
相关标签/搜索