Golang import本地包和导入问题相关详解

1 本地包声明

包是Go程序的基本单位,所以每个Go程序源代码的开始都是一个包声明:

package pkgName

这就是包声明,pkgName 告诉编译器,当前文件属于哪个包。一个包可以对应多个*.go源文件,标记它们属于同一包的唯一依据就是这个package声明,也就是说:无论多少个源文件,只要它们开头的package包相同,那么它们就属于同一个包,在编译后就只会生成一个.a文件,并且存放在$GOPATH/pkg文件夹下。

示例:

(1) 我们在$GOPATH/目录下,创建如下结构的文件夹和文件:

分别写入如下的代码:

hello.go

//hello.go
package hello

import (
  "fmt"
)

func SayHello() {
  fmt.Println("SayHello()-->Hello")
}

hello2.go

//hello2.go
package hello

import (
  "fmt"
)

func SayWorld() {
  fmt.Println("SayWorld()-->World")
}

main.go

//main.go
package main

import (
  "hello"
)

func main() {
  hello.SayHello()
  hello.SayWorld()
}

分析:

根据hello.go/hello2.go中的package声明可知,它们俩属于同一个包–hello,那么根据分析,编译后只会生成一个*.a文件。

执行命令:

go install hello

该命令的意思大概是:编译并安装hello包,这里安装的意思是将生成的*.a文件放到工作目录$GOPATH/pkg目录下去

运行后:

从结果看出,果然只生成了一个包,并且名为hello.a

那么我们提出第二个问题:生成的*.a文件名是否就是我们定义的包名+.a后缀?

为了验证这个问题,我们对源码做一些更改:
将hello.go/hello2.go中的package声明改为如下:

package hello_a

在编译安装包之前,先清除上一次生成的包:

go clean -i hello

再次编译安装该包:

go install hello_a

按照“正常推理”,上面这句命令是没什么问题的,因为我们已经将包名改成hello_a了啊,但是实际的运行结果是这样的:

oh~No!!
那么,我们再试试用这条命令:

go install hello

卧槽!!居然成功了!!是不是??

那么我们尝试生成一下可执行程序,看看能不能正常运行呢?

go build main

又报错了!!!

看这报错提示,好像应该改一下main.go源码,那就改成如下吧:

//main.go
package main

import (
  "hello_a"
)

func main() {
  hello_a.SayHello()
  hello_a.SayWorld()
}

改成上面这样也合情合理哈?毕竟我们把包名定义成了hello_a了!
那就再来编译一次吧:

go build main

继续报错!

等等!!有新的发现,对比上两次的报错信息,可见第一次还能能找到hello_a包的,更改源码后居然还TM找不到hello_a包了??
好吧,那咱再改回去,不过这回只改包的导入语句,改成:

import (
  "hello"
)

再次编译:

go build main

卧槽!!居然没报错了!!再运行一下可执行程序:

好吧,终于得到了想要的结果!

那进行到这里能说明什么呢?

(1) 一个包确实可以由多个源文件组成,只要它们开头的包声明一样
(2)一个包对应生成一个*.a文件,生成的文件名并不是包名+.a
(3) go install ××× 这里对应的并不是包名,而是路径名!!
(4) import ××× 这里使用的也不是包名,也是路径名!
(5) ×××××.SayHello() 这里使用的才是包名!

那么问题又来了,我们该如何理解(3)、(4)中的路径名呢?
我觉得,可以这样理解:
这里指定的是该×××路径名就代表了此目录下唯一的包,编译器连接器默认就会去生成或者使用它,而不需要我们手动指明!

好吧,问题又来了,如果一个目录下有多个包可以吗?如果可以,那该怎么编译和使用??

那我们继续改改源代码:

首先,保持hello2.go 不变,改动hello.go为如下代码:

//hello.go
package hello

import (
  "fmt"
)

func SayHello() {
  fmt.Println("SayHello()-->Hello")
}

并且更改main.go的源码如下

//main.go
package main

import (
  "hello"
)

func main() {
  hello.SayHello()
  hello_a.SayWorld()
}

再次清理掉上次生成的可执行程序与包:

go clean -i hello
go clean -x main

你可以试着执行如上的命令,如果不能清除,那就手动删除吧!
反正,还原成如下样子:

那么再次尝试编译并安装包,不过注意了,此时hello目录下有两个包了,不管是否正确,我们先尝试一下:

go install hello

oh~~果然出错了!!

 
看到了吗?它说它找到了两个包了啊!!!

那这能说明什么呢??

其实这就更加确定的说明了,我们上面的推测是正确的!

(3) go install ××× 这里对应的并不是包名,而是路径名!!

这里指定的是该×××路径名就代表了此目录下唯一的包,编译器连接器默认就会去生成或者使用它,而不需要我们手动指明!

好吧,证明了这个还是挺兴奋的!!那我们继续!!

如果一个目录下,真的有两个或者更多个包,那该如何生成??
抱着试一试的态度,我尝试了许多可能,但无一正确,最后一个命令的结果是让我崩溃的:

go help install

恩!对!你没有看错:installs the packages named by the import paths
What the fuck!! 以后还是决定要先看文档再自己做测试!!

好吧,综上所述,一个目录下就只能有一个包吧,因为都是指定路径,没有办法指定路径下的某个具体的包,这样的做法其实也挺好,让源代码结构更清晰!

2 包的导入问题

导入包:

  • 标准包使用的是给定的短路径,如"fmt"、"net/http"
  • 自己的包,需要在工作目录(GOPATH)下指定一个目录,improt 导入包,实际上就是基于工作目录的文件夹目录

导入包的多种方式:

  • 直接根据$GOPATH/src目录导入import "test/lib"(路径其实是$GOPATH/src/test/lib)
  • 别名导入:import alias_name "test/lib" ,这样使用的时候,可以直接使用别名
  • 使用点号导入:import . "test/lib",作用是使用的时候直接省略包名
  • 使用下划线导入:improt _ "test/lib",该操作其实只是引入该包。当导入一个包时,它所有的init()函数就会被执行,但有些时候并非真的需要使用这些包,仅仅是希望它的init()函数被执行而已。这个时候就可以使用_操作引用该包。即使用_操作引用包是无法通过包名来调用包中的导出函数,而是只是为了简单的调用其init函数()。往往这些init函数里面是注册自己包里面的引擎,让外部可以方便的使用,例如实现database/sql的包,在init函数里面都是调用了sql.Register(name string, driver driver.Driver)注册自己,然后外部就可以使用了。
  • 相对路径导入     import   "./model"  //当前文件同一目录的model目录,但是不建议这种方式import

首先,还是对上面的示例程序做一个更改,这次我们让它变得更加简单点,因为接下来讨论的东西,可能会稍微有点绕~~

首先,删除hello2.go,清理掉编译生成的文件,其他文件内容如下:

hello.go

//hello.go
package hello

import (
  "fmt"
)

func SayHello() {
  fmt.Println("SayHello()-->Hello")
}

main.go

//main.go
package main

import (
  "hello"
)

func main() {
  hello.SayHello()
}

最后,让整体保持如下的样式:

我们先编译一次,让程序能够运行起来:

go install hello
go build main
./main

好吧,假如你能看到输出,那就没问题了!
此时,再来看看整体的结构:

按照C/C++的方式来说,此时生成了hello.a这个链接库,那么源文件那些应该就没有必要了吧,所以。。。。我们这样搞一下,我们来更改一下hello.go源码,但不编译它!
hello.go

//hello.go
package hello

import (
  "fmt"
)

func SayHello() {
  fmt.Println("SayHello()-->Hello_modifi...")
}

然后,我们删除之前的可执行文件main,再重新生成它:

rm main
go build main

恩~~等等,我看一下运行结果:

What the fuck!!!为什么出来的是这货???

好吧,为了一探究竟,我们再次删除main文件,并再次重新编译,不过命令上得做点手脚,我们要看看编译器连接器这两个小婊砸到底都干了些什么,为啥是隔壁老王的儿子出来了??!!

rm main
go build -x -v main

结果:

那么我们一步一步对这个结果做一个分析:

#首先,它好像指定了一个临时工作目录
WORK=/tmp/go-build658882358 

#看着样子,它好像是要准备编译hello目录下的包
hello
#然后创建了一系列临时文件夹
mkdir -p $WORK/hello/_obj/
mkdir -p $WORK/

#进入包的源文件目录
cd /home/yuxuan/GoProjects/import/src/hello 

#调用6g这个编译器编译生成hello.a,存放在$WORK/临时目录下
/opt/go/pkg/tool/linux_amd64/6g -o $WORK/hello.a -trimpath $WORK -p hello -complete -D _/home/yuxuan/GoProjects/import/src/hello -I $WORK -pack ./hello.go

#要编译main目录下的包了
main
#还是创建一系列的临时文件夹
mkdir -p $WORK/main/_obj/
mkdir -p $WORK/main/_obj/exe/

#进入main文件夹
cd /home/yuxuan/GoProjects/import/src/main

#调用6g编译器,编译生成main.a,存放于$WORK/临时目录下
/opt/go/pkg/tool/linux_amd64/6g -o $WORK/main.a -trimpath $WORK -p main -complete -D _/home/yuxuan/GoProjects/import/src/main -I $WORK -I /home/yuxuan/GoProjects/import/pkg/linux_amd64 -pack ./main.go

#最后它进入了一个“当前目录”,应该就是我们执行go build命令的目录
cd .

#调用连接器6l 然后它链接生成a.out,存放与临时目录下的$WORK/main/_obj/exe/文件夹中,但是在链接选项中并未直接发现hello.a
#从链接选项:-L $WORK -L /home/yuxuan/GoProjects/import/pkg/linux_amd64中可以看出,连接器首先搜索了$WORK临时目录下的所有*.a文件,然后再去搜索/home/yuxuan/GoProjects/import/pkg/linux_amd64目录下的*.a文件,可见原因
/opt/go/pkg/tool/linux_amd64/6l -o $WORK/main/_obj/exe/a.out -L $WORK -L /home/yuxuan/GoProjects/import/pkg/linux_amd64 -extld=gcc $WORK/main.a

#最后,移动可执行文件并重命名
mv $WORK/main/_obj/exe/a.out main

到这里,其实差不多也就得出结论了,连接器在连接时,其实使用的并不是我们工作目录下的hello.a文件,而是以该最新源码编译出的临时文件夹中的hello.a文件。

当然,如果你对这个结论有所怀疑,可以试试手动执行上述命令,在最后链接时,去掉-L $WORK的选项,再看看运行结果!

那么,这是对于有源代码的第三方库,如果没有源代码呢?

其实,结果显而易见,没有源代码,上面的临时编译不可能成功,那么临时目录下就不可能有.a文件,所以最后链接时就只能链接到工作目录下的.a文件!

但是,如果是自带的Go标准库呢?

其实也可以用上述的方法验证一下,验证过程就不写了吧?
最后得到的结果是:对于标准库,即便是修改了源代码,只要不重新编译Go源码,那么链接时使用的就还是已经编译好的*.a文件!

3 导入包的三种模式

包导入有三种模式:正常模式、别名模式、简便模式

Go language specification中关于import package时列举的一个例子如下:

Import declaration Local name of Sin

import “lib/math” math.Sin
import m “lib/math” m.Sin
import . “lib/math” Sin

我们看到import m “lib/math” m.Sin一行,在上面的结论中说过lib/math是路径,import语句用m替代lib/math,并在代码中通过m访问math包中导出的函数Sin。
那m到底是包名还是路径呢?
答案显而易见,能通过m访问Sin,那m肯定是包名了!
那问题又来了,import m “lib/math”该如何理解呢?

根据上面得出的结论,我们尝试这样理解m:m指代的是lib/math路径下唯一的那个包!

4 总结

经过上面这一长篇大论,是时候该总结一下成果了:

多个源文件可同属于一个包,只要声明时package指定的包名一样;一个包对应生成一个*.a文件,生成的文件名并不是包名+.a组成,应该是目录名+.a组成go install ××× 这里对应的并不是包名,而是路径名!!import ××× 这里使用的也不是包名,也是路径名×××××.SayHello() 这里使用的才是包名!指定×××路径名就代表了此目录下唯一的包,编译器连接器默认就会去生成或者使用它,而不需要我们手动指明!一个目录下就只能有一个包存在对于调用有源码的第三方包,连接器在连接时,其实使用的并不是我们工作目录下的.a文件,而是以该最新源码编译出的临时文件夹中的.a文件对于调用没有源码的第三方包,上面的临时编译不可能成功,那么临时目录下就不可能有.a文件,所以最后链接时就只能链接到工作目录下的.a文件对于标准库,即便是修改了源代码,只要不重新编译Go源码,那么链接时使用的就还是已经编译好的*.a文件包导入有三种模式:正常模式、别名模式、简便模式

到此这篇关于Golang import本地包和导入问题相关详解的文章就介绍到这了,更多相关Golang import包内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Golang import 导入包语法及一些特殊用法详解

    package 的导入语法 写 Go 代码的时经常用到 import 这个命令用来导入包,参考如下: import( "fmt" ) 然后在代码里面可以通过如下的方式调用: fmt.Println( "我爱我们" ) fmt 是 Go 的标准库,它其实是去 GOROOT 下去加载该模块,当然 Go 的 import 还支持如下两种方式来加载自己写的模块: 相对路径 import "./model" // 当前文件同一目录的 model 目录,但是

  • 对Golang import 导入包语法详解

    package 的导入语法 写 Go 代码的时经常用到 import 这个命令用来导入包,参考如下: import( "fmt" ) 然后在代码里面可以通过如下的方式调用: fmt.Println( "我爱北京天安门" ) fmt 是 Go 的标准库,它其实是去 GOROOT 下去加载该模块,当然 Go 的 import 还支持如下两种方式来加载自己写的模块: 相对路径 import "./model" // 当前文件同一目录的 model 目录

  • Golang import本地包和导入问题相关详解

    1 本地包声明 包是Go程序的基本单位,所以每个Go程序源代码的开始都是一个包声明: package pkgName 这就是包声明,pkgName 告诉编译器,当前文件属于哪个包.一个包可以对应多个*.go源文件,标记它们属于同一包的唯一依据就是这个package声明,也就是说:无论多少个源文件,只要它们开头的package包相同,那么它们就属于同一个包,在编译后就只会生成一个.a文件,并且存放在$GOPATH/pkg文件夹下. 示例: (1) 我们在$GOPATH/目录下,创建如下结构的文件夹

  • Python进阶之import导入机制原理详解

    目录 前言 1. Module组成 1.1 Module 内置全局变量 2. 包package 2.1 实战案例 3.sys.modules.命名空间 3.1 sys.modules 3.2 命名空间 4. 导入 4.1 绝对导入 4.2 相对导入 4.3 单独导入包 5. import运行机制 5.1 标准import,顶部导入 5.2 嵌套import 前言 在Python中,一个.py文件代表一个Module.在Module中可以是任何的符合Python文件格式的Python脚本.了解Mo

  • Java实现Excel导入导出操作详解

    目录 前言 1. 功能测试 1.1 测试准备 1.2 数据导入 1.2.1 导入解析为JSON 1.2.2 导入解析为对象(基础) 1.2.3 导入解析为对象(字段自动映射) 1.2.4 导入解析为对象(获取行号) 1.2.5 导入解析为对象(获取原始数据) 1.2.6 导入解析为对象(获取错误提示) 1.2.7 导入解析为对象(限制字段长度) 1.2.8 导入解析为对象(必填字段验证) 1.2.9 导入解析为对象(数据唯一性验证) 1.3 数据导出 1.3.1 动态导出(基础) 1.3.2 动

  • Golang拾遗之指针和接口的使用详解

    目录 指针和接口 golang的指针 指向interface的指针 总结 指针和接口 golang的类型系统其实很有意思,有意思的地方就在于类型系统表面上看起来众生平等,然而实际上却要分成普通类型(types)和接口(interfaces)来看待.普通类型也包含了所谓的引用类型,例如slice和map,虽然他们和interface同为引用类型,但是行为更趋近于普通的内置类型和自定义类型,因此只有特立独行的interface会被单独归类. 那我们是依据什么把golang的类型分成两类的呢?其实很简

  • Android Studio 一个工程打包多个不同包名的APK实例详解

    公司最近有个特别的需求,同一套代码,稍做修改(如包名不一样,图标不一样,应用名不一样等),编译出几个不同的应用.刚好用AS重构完项目,在网上查阅了一些资料,终于搞定!!在这记录一下. AS主要是利用gradle来实现这个需求的,具体做法如下: 修改app的build.gradle文件 假设我们同一套代码编译2个app:app1和app2 android { ... productFlavors { // app1 app1 { // 设置applicationId(这里很重要,两个相同appli

  • python TCP Socket的粘包和分包的处理详解

    概述 在进行TCP Socket开发时,都需要处理数据包粘包和分包的情况.本文详细讲解解决该问题的步骤.使用的语言是Python.实际上解决该问题很简单,在应用层下,定义一个协议:消息头部+消息长度+消息正文即可. 那什么是粘包和分包呢? 关于分包和粘包 粘包:发送方发送两个字符串"hello"+"world",接收方却一次性接收到了"helloworld". 分包:发送方发送字符串"helloworld",接收方却接收到了两

  • 使用imp和exp命令对Oracle数据库进行导入导出操作详解

    这里导入导出路径都在D盘下,默认文件名为:example.dmp exp方式导出数据 相关参数项如下: 关键字 说明 默认 USERID 用户名/口令 FULL 导出整个文件 (N) BUFFER 数据缓冲区的大小 OWNER 导出指定的所有者用户名列表 FILE 输出文件 (EXPDAT.DMP) TABLES 导出指定的表名列表 COMPRESS 是否压缩导出的文件 (Y) RECORDLENGTH IO 记录的长度 GRANTS 导出权限 (Y) INCTYPE 增量导出类型 INDEXE

  • jmeter在linux系统下运行及本地内存调优的方法详解

    1.在linux系统下安装跨系统传输文件工具 root用户下 根目录输入 yum -y install lrzsz 2.把apache-jmeter-4.0zip包 用rz命令上传到linux系统的根目录下 解压 3.配置jmeter环境变量 vim /etc/profile 添加 export PATH=/apache-jmeter-4.0/bin/:$PATH 注意路径 4.使用 rz命令上传jdk1.8 linux 64位版本 解压到 usr/local 目录下 下载jdk安装包 下载地址

  • python压包的概念及实例详解

    对于一些分解后的元素,我们也是有重新归类的需要.那么我们把解包的恢复过程,叫做压包.这里要用到zip函数的方法,对元素重新进行打包处理,在之前的学习中我们已经对zip函数有所接触.下面我们就python压包的概念.方法进行介绍,然后带来相关的实例使用. 1.概念 压包是解包的逆过程,用zip函数实现. 2.方法 (1)zip() 函数用于将可迭代的对象作为参数,将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的对象(Python3). (2)如果各个迭代器的元素个数不一致,则返回列表长

  • golang fmt格式“占位符”的实例用法详解

    golang 的fmt 包实现了格式化I/O函数,类似于C的 printf 和 scanf. # 定义示例类型和变量 type Human struct { Name string } var people = Human{Name:"zhangsan"} 普通占位符 占位符 说明 举例 输出 %v 相应值的默认格式. Printf("%v", people) {zhangsan}, %+v 打印结构体时,会添加字段名 Printf("%+v",

随机推荐