Golang 包了解以及程序的执行
阅读原文时间:2022年03月26日阅读:1

Golang 包了解以及程序的执行

引言
Go 语言是使用包来组织源代码的,包(package)是多个 Go 源码的集合,是一种高级的代码复用方案。Go 语言中为我们提供了很多内置包,如 fmt、os、io等。

任何源代码文件必须属于某个包,同时源码文件的第一行有效代码必须是package pacakgeName 语句,通过该语句声明自己所在的包。

1. 包的基本概念
Go 语言的包借助了目录树的组织形式,一般包的名称就是其源文件所在目录的名称,虽然Go语言没有强制要求包名必须和其所在的目录名同名,但还是建议包名和所在目录同名,这样结构更清晰。

包可以定义在很深的目录中,包名的定义是不包括目录路径的,但是包在引用时一般使用全路径引用。

比如在GOPATH/src/a/b/ 下定义一个包c。在包c的源码中只需声明为package c,而不是声明为package a/b/c,但是在导入c包时,需要带上路径,例如import "a/b/c"。

2. 包的用法
包名一般是小写的,使用一个简短且有意义的名称
包名一般要和所在的目录同名,也可以不同,包名中不能包含-等特殊符号
包一般使用域名作为目录名称,这样能保证包名的唯一性,比如 GitHub 项目的包一般会放到GOPATH/src/github.com/userName/projectName目录下
包名为 main 的包为应用程序的入口包,编译不包含 main 包的源码文件时不会得到可执行文件
一个文件夹下的所有源码文件只能属于同一个包,同样属于同一个包的源码文件不能放在多个文件夹下。
3. 标识符可见性
在同一个包内部声明的标识符都位于同一个命名空间下,在不同的包内部声明的标识符就属于不同的命名空间。想要在包的外部使用包内部的标识符就需要添加包名前缀,例如fmt.Println("Hello world!"),就是指调用 fmt 包中的Println 函数。

如果想让一个包中的标识符(如变量、常量、类型、函数等)能被外部的包使用,那么标识符必须是对外可见的(public)。在Go语言中是通过标识符的首字母大/小写来控制标识符的对外可见(public)/不可见(private)的。在一个包内部只有首字母大写的标识符才是对外可见的。

例如我们定义一个名为demo的包,在其中定义了若干标识符。在另外一个包中并不是所有的标识符都能通过demo.前缀访问到,因为只有那些首字母是大写的标识符才是对外可见的。

package demo

import "fmt"

// 包级别标识符的可见性

// num 定义一个全局整型变量
// 首字母小写,对外不可见(只能在当前包内使用)
var num = 100

// Mode 定义一个常量
// 首字母大写,对外可见(可在其它包中使用)
const Mode = 1

// person 定义一个代表人的结构体
// 首字母小写,对外不可见(只能在当前包内使用)
type person struct {
name string
Age int
}

// Add 返回两个整数和的函数
// 首字母大写,对外可见(可在其它包中使用)
func Add(x, y int) int {
return x + y
}

// sayHi 打招呼的函数
// 首字母小写,对外不可见(只能在当前包内使用)
func sayHi() {
var myName = "七米" // 函数局部变量,只能在当前函数内使用
fmt.Println(myName)
}

同样的规则也适用于结构体,结构体中可导出字段的字段名称必须首字母大写。

type Student struct {
Name string // 可在包外访问的方法
class string // 仅限包内访问的字段
}

4. 包的引入

  • 要在当前包中使用另外一个包的内容就需要使用import关键字引入这个包,并且 import 语句通常放在文件的开头,package 声明语句的下方。
  • 完整的引入声明语句格式如下:

import importname "path/to/package"

其中:

importname:引入的包名,通常都省略。默认值为引入包的包名。

path/to/package:引入包的路径名称,必须使用双引号包裹起来。

Go语言中禁止循环导入包

  • 一个Go源码文件中可以同时引入多个包,例如:

import "fmt"
import "net/http"
import "os"

  • 当然可以使用批量引入的方式

import (
"fmt"
"net/http"
"os"
)

当引入的多个包中存在相同的包名或者想自行为某个引入的包设置一个新包名时,都需要通过importname指定一个在当前文件中使用的新包名。例如,在引入fmt包时为其指定一个新包名f。

import f "fmt"

这样在当前这个文件中就可以通过使用f来调用fmt包中的函数了。

f.Println("Hello world!")

如果引入一个包的时候为其设置了一个特殊_作为包名,那么这个包的引入方式就称为匿名引入。一个包被匿名引入的目的主要是为了加载这个包,从而使得这个包中的资源得以初始化。 被匿名引入的包中的init函数将被执行并且仅执行一遍。

import _ "github.com/go-sql-driver/mysql"

匿名引入的包与其他方式导入的包一样都会被编译到可执行文件中。
需要注意的是,Go语言中不允许引入包却不在代码中使用这个包的内容,如果引入了未使用的包则会触发编译错误。

1. 标准库概述
标准库API

在 Go 的安装文件里包含了一些可以直接使用的包,即标准库。
在Windows下,标准库的位置在 Go 根目录下的子目录 pkg\windows_amd64中 ; 在 Linux下,标准库在 Go 根目录下的子目录 pkg\linux_amd64中(如果是安装的是32位,则在(linux_386目录中)。
一般情况下,标准包会存放在$GOROOT/pkg/$GOOS_$GOARCH/目录下。

Go 的标准库包含了大量的包(如: fmt和os),但是也可以创建自己的包。

如果想要构建一个程序,则包和包内的文件都必须以正确的顺序进行编译。包的依赖关系决定了其构建顺序。属于同一个包的源文件必须全部被一起编译,一个包即是编译时的一个单元,因此根据惯例,每个目录都只包含一个包。

如果对一个包进行更改或重新编译,所有引用了这个包的客户端程序都必须全部重新编译。

Go 中的包模型采用了显式依赖关系的机制来达到快速编译的目的,编译器会从后缀名为.o的对象文件(需要且只需要这个文件)中提取传递依赖类型的信息。

如果A.go依赖B.go,而B.go又依赖c.go:
编译c.go,B.go,然后是A.go
为了编译A.go,编译器读取的是 B.o 而不是c.o

这种机制对于编译大型的项目时可以显著地提升编译速度。

示例:
一个程序包含两个包: cat和main,其中 add 包中包含两个变量 Name 和 Age,请问 main 包中如何访问 Name 和 Age。

package cat

import "fmt"

var Name string = "tom"
var Age int = 5

//初始化函数
func init() {
fmt.Println("this is cat package")
fmt.Println("init函数修改前:", Name, Age)
Name = "jack"
Age = 3
fmt.Println("init函数修改后:", Name, Age)
}

package main

import (
//包的别名定义
a "dev_code/day9/example3/cat"
b "fmt"
)

func main() {
b.Println("猫的名字:", a.Name)
b.Println("猫的年龄:", a.Age)
}

输出结果如下

this is cat package
init函数修改前: tom 5
init函数修改后: jack 3
猫的名字: jack
猫的年龄: 3

  • 总结:
    调用其他包程序加载顺序:
    cat.go中的全局变量----->cat.go中的init()函数--------->main.go中的main()函数

2. 标准库常见的包及其功能

  • Go语言的标准库以包的方式提供支持,下表列出了Go语言标准库中常见的包及其功能。

Go语言标准库包名

功 能

bufio

带缓冲的 I/O 操作

bytes

实现字节操作

container

封装堆、列表和环形列表等容器

crypto

加密算法

database

数据库驱动和接口

debug

各种调试文件格式访问及调试功能

encoding

常见算法如 JSON、XML、Base64 等

flag

命令行解析

fmt

格式化操作

go

Go语言的词法、语法树、类型等。可通过这个包进行代码信息提取和修改

html

HTML 转义及模板系统

image

常见图形格式的访问及生成

io

实现 I/O 原始访问接口及访问封装

math

数学库

net

网络库,支持 Socket、HTTP、邮件、RPC、SMTP 等

os

操作系统平台不依赖平台操作封装

path

兼容各操作系统的路径操作实用函数

plugin

Go 1.7 加入的插件系统。支持将代码编译为插件,按需加载

reflect

语言反射支持。可以动态获得代码中的类型信息,获取和修改变量的值

regexp

正则表达式封装

runtime

运行时接口

sort

排序接口

strings

字符串转换、解析及实用函数

time

时间接口

text

文本模板及 Token 词法器

3. 程序执行顺序

Go 程序的执行(程序启动)顺序如下:
① 按顺序导入所有被 main 包引用的其它包,然后在每个包中执行如下流程:
② 如果该包又导入了其它的包,则从第一步开始递归执行,但是每个包只会被导入一次。
③ 然后以相反的顺序在每个包中初始化常量和变量,如果该包含有init 函数的话,则调用该函数。
④ 在完成这一切之后,main 也执行同样的过程,最后调用 main 函数开始执行程序。

demo.go

package demo

import "fmt"

var Name string = "this is demo package"
var Age int = 20

func init() {
fmt.Println("this is demo init()")
fmt.Println("demo.package.Name=", Name)
fmt.Println("demo.package.Age=", Age)
Name = "this is demo New"
Age = 200
fmt.Println("demo.package.Name=", Name)
fmt.Println("demo.package.Age=", Age)
}

main.go

package main

import (
"dev_code/day9/example4/test"
"fmt"
)

func main() {
//main---->test----->demo

fmt.Println("main.package:", test.Name)  
fmt.Println("main.package:", test.Age)  

}

test.go

package test

import (
"fmt"
//对指定包做初始化,并不做调用处理
_ "dev_code/day9/example4/demo"
)

var Name string = "this is test package"
var Age int = 10

func init() {
fmt.Println("this is test init()")
fmt.Println("test.package.Name=", Name)
fmt.Println("test.package.Age=", Age)
Name = "this is test New"
Age = 100
fmt.Println("test.package.Name=", Name)
fmt.Println("test.package.Age=", Age)
}

执行结果如下

this is demo init()
demo.package.Name= this is demo package
demo.package.Age= 20
demo.package.Name= this is demo New
demo.package.Age= 200
this is test init()
test.package.Name= this is test package
test.package.Age= 10
test.package.Name= this is test New
test.package.Age= 100
main.package: this is test New
main.package: 100

包引用的时候顺序:
main引用add包,add再引用demo包;
编译执行流程:
先从demo包这里编译加载里面的全局变量,然后执行init函数,当init函数执行完成以后再去执行add中的全局变量,再执行里面的init函数,最后执行main包中的函数