本文会参照Java来比较分析GO的编译,为了解两者区别,阅读正文前可以先了解下这两个概念
程序的执行,说到底就是将代码编译成平台能运行的机器码,然后执行的过程
执行方式分成了两种:
编译型语言效率高,但跨平台得重新编译程序;解释型语言易跨平台执行,但每次运行要编译效率低。
Golang 是编译型语言
Java是半编译半解释型语言(编译成jvm的字节码,即class文件,然后jvm解释执行)
刚学go语言时,我一直都没有弄懂这个变量到底是做什么的,先看看官方的定义:
GOPATH 环境变量指定了你的工作空间位置。它或许是你在开发Go代码时, 唯一需要设置的环境变量。
Go代码必须放在工作空间内。它其实就是一个目录,其中包含三个子目录:
go 工具用于构建源码包,并将其生成的二进制文件安装到 pkg 和 bin 目录中。src 子目录通常包会含多种版本控制的代码仓库(例如Git或Mercurial), 以此来跟踪一个或多个源码包的开发
总结一下官方的描述重点:
GoPath
中 .a
,java中为 .jar
),bin包含编译后的可执行文件(go中根据平台不一样后缀不一样,java中所有平台都为 .jar
) 疑问一:为啥Go代码必须放在 GOPATH
中
从java转过来的我表示不能理解,为啥规定所有代码都要在 GoPath
目录?万能的java里,项目在任何目录都是可以执行的呀
我们来实验一下,在非 GoPath
创建项目是否可以运行。
D://
创建如下
goProject
项目,包含一个
main.go
,及引用到的
basic.hello.go
代码如下:
main.go:
package main import "basic" func main() { basic.Hello() }
hello.go:
package basic import "fmt" func Hello() { fmt.Print("Hello! ") }
尝试编译main.go文件,发现报错:
PS D:/goProject/src> go build -n main.go main.go:3:8: cannot find package "basic" in any of: D:/Program Files/go/src/basic (from $GOROOT) E:/workspace/go/src/basic (from $GOPATH)
从错误信息中我们了解到,编译失败的原因在于,寻找引用包 basic
时,并没在像我们想象的自动在项目路径src下寻找,而是分别在 $GOROOT
和 $GOPATH
进行了查找。
也就是说,代码只有都放进 $GOPATH
,才能保证import的引用都能正确的被找到。如果你的代码除了官方引用( $GOROOT
),没有其他包的引用,也是可以正常编译运行的。
扩展:为什么Java项目放在任何路径都可以正常编译呢?
Java中有一个类似 GOPATH
的参数 classpath
,它是Java运行时环境搜索类和其他资源文件(比如jar/zip等资源)
的路径。
classpath
默认为jdk的相关目录(lib)和当前目录。java程序编译和运行时,都可以指定classpath。我们之所以感觉java项目可以任意目录执行,是因为idea、maven这些工具帮我们指定好了运行时依赖的classpath路径。(在文章末尾有纯命令编译运行java项目的例子,想要了解的朋友可以简单看看)
Go中其实也可以在项目运行的环境变量中指定 GOPATH
,这样的好处在于每个项目的依赖包相互隔离。
但是个人感觉 GOPATH
的设计理念就是基于想把所有的依赖包、代码、二进制文件统一到一个目录。并且GO这么设计的时候很粗暴的不支持依赖包有不同版本:
Go philosophy is that everything should be backward compatible. If you release a library, you have to ensure it stay compatible with older versions's public API. If you need to break the API, then it is a new package and should have a new import path.
Go设计的哲学思维是,所有的代码都应该向后兼容。如果你发布一个库,你必须保证之前版本的API仍然是可以正常使用的。如果不能,那新版本的API应是在新的包路径中被引用
Go这样的设计应该是没有得到很大的认可,所以在后续的版本中,Go还是加入了依赖包的版本管理(go 1.11和1.12版本中新增了go module)
我们上面的项目放入 $GOPATH
后编译,得到如下可执行文件:
image.png
一个hello world级别的代码,编译出来的可执行文件居然快2M?我们将java程序打包成可执行jar包,也不会这么大呀。
我们来详细看下 main.go
的编译过程:
E:/workspace/go/src/github.com/Mrdshu/codeDemo/goDemo> go build -x main.go WORK=C:/Users/xxx/AppData/Local/Temp/go-build130222670 ----(指定临时编译目录) mkdir -p $WORK/b002/ cat >$WORK/b002/importcfg << 'EOF' # internal # import config packagefile fmt=D:/Program Files/go/pkg/windows_amd64/fmt.a EOF cd E:/workspace/go/src/github.com/Mrdshu/codeDemo/goDemo/basic ----(compile依赖包,并将编译好的归档文件pkg_.a文件复制到缓存目录,注意并不是$GOPATH/pkg目录) "D://Program Files//go//pkg//tool//windows_amd64//compile.exe" -o "C://Users//xxx//AppData//Local//Temp//go-build130222670//b002//_pkg_.a" -trimpath "C://Users//xxx//AppData//Local//Temp//go-build130222670//b002" -p github.com/Mrdshu/codeDemo/goDemo/basic -complete -buildid G_nVir86m2b03lvrDAWh/G_nVir86m2b03lvrDAWh -goversion go1.11 -D "" -importcfg "C://Users//xxx//AppData//Local//Temp//go-build130222670//b002//importcfg" -pack -c=4 "E://workspace//go//src//github.com//Mrdshu//codeDemo//goDemo//basic//type.go" "D://Program Files//go//pkg//tool//windows_amd64//buildid.exe" -w "C://Users//xxx//AppData//Local//Temp//go-build130222670//b002//_pkg_.a" # internal cp "C://Users//xxx//AppData//Local//Temp//go-build130222670//b002//_pkg_.a" "C://Users//xxx//AppData//Local//go-build//50//5026df2f79f2ef5ea4775d8d700e1ab5086453e6fe91b37d372e005c5655a6fc-d" # internal mkdir -p $WORK/b001/ cat >$WORK/b001/importcfg << 'EOF' # internal # import config packagefile github.com/Mrdshu/codeDemo/goDemo/basic=$WORK/b002/_pkg_.a ----(指定import依赖包的) packagefile runtime=D:/Program Files/go/pkg/windows_amd64/runtime.a EOF cd E:/workspace/go/src/github.com/Mrdshu/codeDemo/goDemo ----(编译main.go) "D://Program Files//go//pkg//tool//windows_amd64//compile.exe" -o "C://Users//xxx//AppData//Local//Temp//go-build130222670//b001//_pkg_.a" -trimpath "C://Users//xxx//AppData//Local//Temp//go-build130222670//b001" -p main -complete -buildid vFLkQqJe-TIZKPTKaKqi/vFLkQqJe-TIZKPTKaKqi -goversion go1.11 -D _/E_/workspace/go/src/github.com/Mrdshu/codeDemo/goDemo -importcfg "C://Users//xxx//AppData//Local//Temp//go-build130222670//b001//importcfg" -pack -c=4 "E://workspace//go//src//github.com//Mrdshu//codeDemo//goDemo//main.go" "D://Program Files//go//pkg//tool//windows_amd64//buildid.exe" -w "C://Users//xxx//AppData//Local//Temp//go-build130222670//b001//_pkg_.a" # internal cp "C://Users//xxx//AppData//Local//Temp//go-build130222670//b001//_pkg_.a" "C://Users//xxx//AppData//Local//go-build//89//89f263733604869aad0807de6f81086dc556c2e81ace89d9b484618fb0d5d586-d" # internal cat >$WORK/b001/importcfg.link << 'EOF' # internal ----(以下是链接地址) packagefile command-line-arguments=$WORK/b001/_pkg_.a packagefile github.com/Mrdshu/codeDemo/goDemo/basic=$WORK/b002/_pkg_.a packagefile runtime=D:/Program Files/go/pkg/windows_amd64/runtime.a packagefile fmt=D:/Program Files/go/pkg/windows_amd64/fmt.a packagefile internal/bytealg=D:/Program Files/go/pkg/windows_amd64/internal/bytealg.a packagefile internal/cpu=D:/Program Files/go/pkg/windows_amd64/internal/cpu.a packagefile runtime/internal/atomic=D:/Program Files/go/pkg/windows_amd64/runtime/internal/atomic.a packagefile runtime/internal/sys=D:/Program Files/go/pkg/windows_amd64/runtime/internal/sys.a packagefile errors=D:/Program Files/go/pkg/windows_amd64/errors.a packagefile io=D:/Program Files/go/pkg/windows_amd64/io.a packagefile math=D:/Program Files/go/pkg/windows_amd64/math.a packagefile os=D:/Program Files/go/pkg/windows_amd64/os.a packagefile reflect=D:/Program Files/go/pkg/windows_amd64/reflect.a packagefile strconv=D:/Program Files/go/pkg/windows_amd64/strconv.a packagefile sync=D:/Program Files/go/pkg/windows_amd64/sync.a packagefile unicode/utf8=D:/Program Files/go/pkg/windows_amd64/unicode/utf8.a packagefile sync/atomic=D:/Program Files/go/pkg/windows_amd64/sync/atomic.a packagefile internal/poll=D:/Program Files/go/pkg/windows_amd64/internal/poll.a packagefile internal/syscall/windows=D:/Program Files/go/pkg/windows_amd64/internal/syscall/windows.a packagefile internal/testlog=D:/Program Files/go/pkg/windows_amd64/internal/testlog.a packagefile syscall=D:/Program Files/go/pkg/windows_amd64/syscall.a packagefile time=D:/Program Files/go/pkg/windows_amd64/time.a packagefile unicode/utf16=D:/Program Files/go/pkg/windows_amd64/unicode/utf16.a packagefile unicode=D:/Program Files/go/pkg/windows_amd64/unicode.a packagefile math/bits=D:/Program Files/go/pkg/windows_amd64/math/bits.a packagefile internal/race=D:/Program Files/go/pkg/windows_amd64/internal/race.a packagefile internal/syscall/windows/sysdll=D:/Program Files/go/pkg/windows_amd64/internal/syscall/windows/sysdll.a packagefile internal/syscall/windows/registry=D:/Program Files/go/pkg/windows_amd64/internal/syscall/windows/registry.a EOF mkdir -p $WORK/b001/exe/ cd . ----(编译链接,得到可执行文件) "D://Program Files//go//pkg//tool//windows_amd64//link.exe" -o "C://Users//xxx//AppData//Local//Temp//go-build130222670//b001//exe//a.out.exe" -importcfg "C://Users//xxx//AppData//Local//Temp//go-build130222670//b001//importcfg.link" -buildmode=exe -buildid=y5HJgFD5pvt8M7MF2-zO/vFLkQqJe-TIZKPTKaKqi/P1SUhx5DB4gHx03j6wd9/y5HJgFD5pvt8M7MF2-zO -extld=gcc "C://Users//xxx//AppData//Local//Temp//go-build130222670//b001//_pkg_.a" "D://Program Files//go//pkg//tool//windows_amd64//buildid.exe" -w "C://Users//xxx//AppData//Local//Temp//go-build130222670//b001//exe//a.out.exe" # internal cp $WORK/b001/exe/a.out.exe main.exe rm -r $WORK/b001/
从如上的编译过程,我们可以大致的知道:
Local//go-build main.go
java的jar包之所以小是因为只包含了真正源代码的字节码(class文件),等到jvm运行时才编译链接成二进制文件,最终执行;
而go程序编译时链接了go语言底层的代码库,不单单只有源代码。
最后,引用 官方文档 中的解释来进一步理解:
Why is my trivial program such a large binary?
The linker in the gc toolchain creates statically-linked binaries by default. All Go binaries therefore include the Go runtime, along with the run-time type information necessary to support dynamic type checks, reflection, and even panic-time stack traces.
A simple C "hello, world" program compiled and linked statically using gcc on Linux is around 750 kB, including an implementation of printf. An equivalent Go program using fmt.Printf weighs a couple of megabytes, but that includes more powerful run-time support and type and debugging information.
A Go program compiled with gc can be linked with the -ldflags=-w flag to disable DWARF generation, removing debugging information from the binary but with no other loss of functionality. This can reduce the binary size substantially.
疑问三:为什么编译后 GOPATH
的 pkg
、 bin
中没有文件?
上面我们编译命令源码文件 main.go
后,得到的二进制可执行文件在当前目录,并不是 bin
目录。而编译依赖包后生成的归档文件也不在 pkg
目录,而是在缓存目录。
那 pkg
、 bin
目录下什么时候有文件呢?
答案是使用 go install
时。有兴趣的朋友可以尝试使用 go install -x
来观察执行过程, go install
过程和 go build
差不多,只是最后多了一行命令将生成的文件移动到 bin
或 pkg
中。
另外,当pkg目录、缓存目录同时存在依赖包的归档文件时,编译器会使用pkg目录下的归档文件。
不用 maven
、 grade
等项目管理工具,我们看一个“原生态”的java项目的结构:
如图可分为 src
(存放源码)、 target
(存放编译后的class文件)、 lib
(存放第三方引用jar包)三个目录。
package packageA; public class A { private String name; public A(String name) { this.name = name; } public String getName() { return name; } }
package packageA; import org.springframework.util.StringUtils; public class Main { public static void main(String[] args) { A a = new A("aaaa"); String name = a.getName(); if (StringUtils.isEmpty(name)){ System.out.println("name is empty"); } else{ System.out.println("name is "+name); } } }
我们直接在源码路径编译项目:
PS D:/project/src/packageA> javac -verbose -encoding UTF-8 -classpath "D:/project/lib/spring-core-5.0.7.RELEASE.jar" -d D:/project/target/classes A.java Main.java [解析开始时间 RegularFileObject[A.java]] [解析已完成, 用时 18 毫秒] [解析开始时间 RegularFileObject[Main.java]] [解析已完成, 用时 2 毫秒] [源文件的搜索路径: D:/project/lib/spring-core-5.0.7.RELEASE.jar] [类文件的搜索路径: D:/Program Files/Java/jdk1.8.0/jre/lib/resources.jar,D:/Program Files/Java/jdk1.8.0/jre/lib/rt.jar,D:/Program Files/Java/jdk1.8.0/jre/lib/sunrsasign.jar,D:/Program Files/Java/jdk1.8.0/jre/lib/jsse.jar,D:/Program Files/Java/jdk1.8.0/jre/lib/jce.jar,D:/Program Files/Java/jdk1.8.0/jre/lib/charsets.jar,D:/Program Files/Java/jdk1.8.0/jre/lib/jfr.jar,D:/Program Files/Java/jdk1.8.0/jre/classes,D:/Program Files/Java/jdk1.8.0/jre/lib/ext/access-bridge-64.jar,D:/Program Files/Java/jdk1.8.0/jre/lib/ext/cldrdata.jar,D:/Program Files/Java/jdk1.8.0/jre/lib/ext/dnsns.jar,D:/Program Files/Java/jdk1.8.0/jre/lib/ext/jaccess.jar,D:/Program Files/Java/jdk1.8.0/jre/lib/ext/jfxrt.jar,D:/Program Files/Java/jdk1.8.0/jre/lib/ext/localedata.jar,D:/Program Files/Java/jdk1.8.0/jre/lib/ext/nashorn.jar,D:/Program Files/Java/jdk1.8.0/jre/lib/ext/sunec.jar,D:/Program Files/Java/jdk1.8.0/jre/lib/ext/sunjce_provider.jar,D:/Program Files/Java/jdk1.8.0/jre/lib/ext/sunmscapi.jar,D:/Program Files/Java/jdk1.8.0/jre/lib/ext/sunpkcs11.jar,D:/Program Files/Java/jdk1.8.0/jre/lib/ext/zipfs.jar,D:/project/lib/spring-core-5.0.7.RELEASE.jar] [正在加载ZipFileIndexFileObject[D:/Program Files/Java/jdk1.8.0/lib/ct.sym(META-INF/sym/rt.jar/java/lang/Object.class)]] [正在加载ZipFileIndexFileObject[D:/Program Files/Java/jdk1.8.0/lib/ct.sym(META-INF/sym/rt.jar/java/lang/String.class)]] [正在加载ZipFileIndexFileObject[D:/project/lib/spring-core-5.0.7.RELEASE.jar(org/springframework/util/StringUtils.class)]] [正在检查packageA.A] [正在加载ZipFileIndexFileObject[D:/Program Files/Java/jdk1.8.0/lib/ct.sym(META-INF/sym/rt.jar/java/io/Serializable.class)]] [正在加载ZipFileIndexFileObject[D:/Program Files/Java/jdk1.8.0/lib/ct.sym(META-INF/sym/rt.jar/java/lang/AutoCloseable.class)]] [已写入RegularFileObject[D:/project/target/classes/packageA/A.class]] [正在检查packageA.Main] [正在加载ZipFileIndexFileObject[D:/Program Files/Java/jdk1.8.0/lib/ct.sym(META-INF/sym/rt.jar/java/lang/Byte.class)]] [正在加载ZipFileIndexFileObject[D:/Program Files/Java/jdk1.8.0/lib/ct.sym(META-INF/sym/rt.jar/java/lang/Character.class)]] [正在加载ZipFileIndexFileObject[D:/Program Files/Java/jdk1.8.0/lib/ct.sym(META-INF/sym/rt.jar/java/lang/Short.class)]] [正在加载ZipFileIndexFileObject[D:/Program Files/Java/jdk1.8.0/lib/ct.sym(META-INF/sym/rt.jar/java/lang/Long.class)]] [正在加载ZipFileIndexFileObject[D:/Program Files/Java/jdk1.8.0/lib/ct.sym(META-INF/sym/rt.jar/java/lang/Float.class)]] [正在加载ZipFileIndexFileObject[D:/Program Files/Java/jdk1.8.0/lib/ct.sym(META-INF/sym/rt.jar/java/lang/Integer.class)]] [正在加载ZipFileIndexFileObject[D:/Program Files/Java/jdk1.8.0/lib/ct.sym(META-INF/sym/rt.jar/java/lang/Double.class)]] [正在加载ZipFileIndexFileObject[D:/Program Files/Java/jdk1.8.0/lib/ct.sym(META-INF/sym/rt.jar/java/lang/Boolean.class)]] [正在加载ZipFileIndexFileObject[D:/Program Files/Java/jdk1.8.0/lib/ct.sym(META-INF/sym/rt.jar/java/lang/Void.class)]] [正在加载ZipFileIndexFileObject[D:/Program Files/Java/jdk1.8.0/lib/ct.sym(META-INF/sym/rt.jar/java/lang/System.class)]] [正在加载ZipFileIndexFileObject[D:/Program Files/Java/jdk1.8.0/lib/ct.sym(META-INF/sym/rt.jar/java/io/PrintStream.class)]] [正在加载ZipFileIndexFileObject[D:/Program Files/Java/jdk1.8.0/lib/ct.sym(META-INF/sym/rt.jar/java/lang/Appendable.class)]] [正在加载ZipFileIndexFileObject[D:/Program Files/Java/jdk1.8.0/lib/ct.sym(META-INF/sym/rt.jar/java/io/Closeable.class)]] [正在加载ZipFileIndexFileObject[D:/Program Files/Java/jdk1.8.0/lib/ct.sym(META-INF/sym/rt.jar/java/io/FilterOutputStream.class)]] [正在加载ZipFileIndexFileObject[D:/Program Files/Java/jdk1.8.0/lib/ct.sym(META-INF/sym/rt.jar/java/io/OutputStream.class)]] [正在加载ZipFileIndexFileObject[D:/Program Files/Java/jdk1.8.0/lib/ct.sym(META-INF/sym/rt.jar/java/io/Flushable.class)]] [正在加载ZipFileIndexFileObject[D:/Program Files/Java/jdk1.8.0/lib/ct.sym(META-INF/sym/rt.jar/java/lang/Comparable.class)]] [正在加载ZipFileIndexFileObject[D:/Program Files/Java/jdk1.8.0/lib/ct.sym(META-INF/sym/rt.jar/java/lang/CharSequence.class)]] [正在加载ZipFileIndexFileObject[D:/Program Files/Java/jdk1.8.0/lib/ct.sym(META-INF/sym/rt.jar/java/lang/StringBuilder.class)]] [正在加载ZipFileIndexFileObject[D:/Program Files/Java/jdk1.8.0/lib/ct.sym(META-INF/sym/rt.jar/java/lang/AbstractStringBuilder.class)]] [正在加载ZipFileIndexFileObject[D:/Program Files/Java/jdk1.8.0/lib/ct.sym(META-INF/sym/rt.jar/java/lang/StringBuffer.class)]] [已写入RegularFileObject[D:/project/target/classes/packageA/Main.class]] [共 258 毫秒]
classpath
默认为 .;%JAVA_HOME%/lib;%JAVA_HOME%/lib/tools.jar
A.java
,再编译使用到 A
的 Main.java
通过日志我们可以清晰的看到,java程序编译时,到指定的classpath路径下搜索用到的源文件和类文件,然后找到依赖的class文件并引用,最终在指定的 /target/classes
目录生成了对应的class文件。
在 target/classes
目录我们运行程序:
运行时java的类加载器会将class文件加载进去,然后进行链接、初始化,最终执行
java -classpath "D:/project/lib/spring-core-5.0.7.RELEASE.jar;." -verbose packageA/Main ##output:name is aaaa
参考文档:
初探 Go 的编译命令执行过程
Can someone explain why GOPATH is convenient and how it should be used in general?