编译语言 vs 解释语言

本文会参照Java来比较分析GO的编译,为了解两者区别,阅读正文前可以先了解下这两个概念

程序的执行,说到底就是将代码编译成平台能运行的机器码,然后执行的过程
执行方式分成了两种:

  1. 编译型:通过编译器,将代码编译成平台特定的机器码。编译与运行隔开,一次编译,可多次运行。代表有C、C++
  2. 解释型:通过解释器,逐行编译代码成平台的机器码,并立即运行。即每次运行时都编译。代表有Python、Ruby

编译型语言效率高,但跨平台得重新编译程序;解释型语言易跨平台执行,但每次运行要编译效率低。

Golang 是编译型语言
Java是半编译半解释型语言(编译成jvm的字节码,即class文件,然后jvm解释执行)

GOPATH定义

刚学go语言时,我一直都没有弄懂这个变量到底是做什么的,先看看官方的定义:

GOPATH 环境变量指定了你的工作空间位置。它或许是你在开发Go代码时, 唯一需要设置的环境变量。

Go代码必须放在工作空间内。它其实就是一个目录,其中包含三个子目录:

  • src 目录包含Go的源文件,它们被组织成包(每个目录都对应一个包),
  • pkg 目录包含包对象,
  • bin 目录包含可执行命令。

go 工具用于构建源码包,并将其生成的二进制文件安装到 pkg 和 bin 目录中。src 子目录通常包会含多种版本控制的代码仓库(例如Git或Mercurial), 以此来跟踪一个或多个源码包的开发

总结一下官方的描述重点:

  1. 所有Go代码必须放在GoPath
  2. src包含所有源代码,pkg包含编译后的包文件(go中后缀为.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:goProjectsrc> go build -n main.go
main.go:3:8: cannot find package "basic" in any of:
        D:Program Filesgosrcbasic (from GOROOT)
        E:workspacegosrcbasic (fromGOPATH)

从错误信息中我们了解到,编译失败的原因在于,寻找引用包basic时,并没在像我们想象的自动在项目路径src下寻找,而是分别在$GOROOT$GOPATH进行了查找。

也就是说,代码只有都放进$GOPATH,才能保证import的引用都能正确的被找到。如果你的代码除了官方引用($GOROOT),没有其他包的引用,也是可以正常编译运行的。

扩展:为什么Java项目放在任何路径都可以正常编译呢?
Java中有一个类似GOPATH的参数classpath,它是Java运行时环境搜索类和其他资源文件(比如jarzip等资源)
的路径。
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)

疑问二:为啥Go编译后的可执行文件那么大?

我们上面的项目放入$GOPATH后编译,得到如下可执行文件:

image.png

一个hello world级别的代码,编译出来的可执行文件居然快2M?我们将java程序打包成可执行jar包,也不会这么大呀。

我们来详细看下main.go的编译过程:

 E:workspacegosrcgithub.comMrdshucodeDemogoDemo> go build -x main.go
WORK=C:UsersxxxAppDataLocalTempgo-build130222670  ----(指定临时编译目录)
mkdir -p WORKb002
cat >WORKb002importcfg WORKb001importcfgWORKb001importcfg.link 

从如上的编译过程,我们可以大致的知道:

  1. go build会先编译依赖包,并将编译的归档文件最终放入一个Local\go-build的缓存目录
  2. 编译命令源码文件main.go时,除了链接缓存目录下的依赖包外,还链接了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.

疑问三:为什么编译后GOPATHpkgbin中没有文件?

上面我们编译命令源码文件main.go后,得到的二进制可执行文件在当前目录,并不是bin目录。而编译依赖包后生成的归档文件也不在pkg目录,而是在缓存目录。

pkgbin目录下什么时候有文件呢?

答案是使用go install时。有兴趣的朋友可以尝试使用go install -x来观察执行过程,go install过程和go build差不多,只是最后多了一行命令将生成的文件移动到binpkg中。

另外,当pkg目录、缓存目录同时存在依赖包的归档文件时,编译器会使用pkg目录下的归档文件。

附加:Java 项目的编译示例

不用mavengrade等项目管理工具,我们看一个“原生态”的java项目的结构:

如图可分为src(存放源码)、target(存放编译后的class文件)、lib(存放第三方引用jar包)三个目录。

A.java

package packageA;

public class A {
    private String name;

    public A(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

Main.java

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:projectsrcpackageA> javac -verbose  -encoding UTF-8 -classpath "D:projectlibspring-core-5.0.7.RELEASE.jar" -d
  D:projecttargetclasses A.java Main.java
[解析开始时间 RegularFileObject[A.java]]
[解析已完成, 用时 18 毫秒]
[解析开始时间 RegularFileObject[Main.java]]
[解析已完成, 用时 2 毫秒]
[源文件的搜索路径: D:projectlibspring-core-5.0.7.RELEASE.jar]
[类文件的搜索路径: D:Program FilesJavajdk1.8.0jrelibresources.jar,D:Program FilesJavajdk1.8.0jrelibrt.jar,D:Program FilesJavajdk1.8.0jrelibsunrsasign.jar,D:Program FilesJavajdk1.8.0jrelibjsse.jar,D:Program FilesJavajdk1.8.0jrelibjce.jar,D:Program FilesJavajdk1.8.0jrelibcharsets.jar,D:Program FilesJavajdk1.8.0jrelibjfr.jar,D:Program FilesJavajdk1.8.0jreclasses,D:Program FilesJavajdk1.8.0jrelibextaccess-bridge-64.jar,D:Program FilesJavajdk1.8.0jrelibextcldrdata.jar,D:Program FilesJavajdk1.8.0jrelibextdnsns.jar,D:Program FilesJavajdk1.8.0jrelibextjaccess.jar,D:Program FilesJavajdk1.8.0jrelibextjfxrt.jar,D:Program FilesJavajdk1.8.0jrelibextlocaledata.jar,D:Program FilesJavajdk1.8.0jrelibextnashorn.jar,D:Program FilesJavajdk1.8.0jrelibextsunec.jar,D:Program FilesJavajdk1.8.0jrelibextsunjce_provider.jar,D:Program FilesJavajdk1.8.0jrelibextsunmscapi.jar,D:Program FilesJavajdk1.8.0jrelibextsunpkcs11.jar,D:Program FilesJavajdk1.8.0jrelibextzipfs.jar,D:projectlibspring-core-5.0.7.RELEASE.jar]
[正在加载ZipFileIndexFileObject[D:Program FilesJavajdk1.8.0libct.sym(META-INF/sym/rt.jar/java/lang/Object.class)]]
[正在加载ZipFileIndexFileObject[D:Program FilesJavajdk1.8.0libct.sym(META-INF/sym/rt.jar/java/lang/String.class)]]
[正在加载ZipFileIndexFileObject[D:projectlibspring-core-5.0.7.RELEASE.jar(org/springframework/util/StringUtils.class)]]
[正在检查packageA.A]
[正在加载ZipFileIndexFileObject[D:Program FilesJavajdk1.8.0libct.sym(META-INF/sym/rt.jar/java/io/Serializable.class)]]
[正在加载ZipFileIndexFileObject[D:Program FilesJavajdk1.8.0libct.sym(META-INF/sym/rt.jar/java/lang/AutoCloseable.class)]]
[已写入RegularFileObject[D:projecttargetclassespackageAA.class]]
[正在检查packageA.Main]
[正在加载ZipFileIndexFileObject[D:Program FilesJavajdk1.8.0libct.sym(META-INF/sym/rt.jar/java/lang/Byte.class)]]
[正在加载ZipFileIndexFileObject[D:Program FilesJavajdk1.8.0libct.sym(META-INF/sym/rt.jar/java/lang/Character.class)]]
[正在加载ZipFileIndexFileObject[D:Program FilesJavajdk1.8.0libct.sym(META-INF/sym/rt.jar/java/lang/Short.class)]]
[正在加载ZipFileIndexFileObject[D:Program FilesJavajdk1.8.0libct.sym(META-INF/sym/rt.jar/java/lang/Long.class)]]
[正在加载ZipFileIndexFileObject[D:Program FilesJavajdk1.8.0libct.sym(META-INF/sym/rt.jar/java/lang/Float.class)]]
[正在加载ZipFileIndexFileObject[D:Program FilesJavajdk1.8.0libct.sym(META-INF/sym/rt.jar/java/lang/Integer.class)]]
[正在加载ZipFileIndexFileObject[D:Program FilesJavajdk1.8.0libct.sym(META-INF/sym/rt.jar/java/lang/Double.class)]]
[正在加载ZipFileIndexFileObject[D:Program FilesJavajdk1.8.0libct.sym(META-INF/sym/rt.jar/java/lang/Boolean.class)]]
[正在加载ZipFileIndexFileObject[D:Program FilesJavajdk1.8.0libct.sym(META-INF/sym/rt.jar/java/lang/Void.class)]]
[正在加载ZipFileIndexFileObject[D:Program FilesJavajdk1.8.0libct.sym(META-INF/sym/rt.jar/java/lang/System.class)]]
[正在加载ZipFileIndexFileObject[D:Program FilesJavajdk1.8.0libct.sym(META-INF/sym/rt.jar/java/io/PrintStream.class)]]
[正在加载ZipFileIndexFileObject[D:Program FilesJavajdk1.8.0libct.sym(META-INF/sym/rt.jar/java/lang/Appendable.class)]]
[正在加载ZipFileIndexFileObject[D:Program FilesJavajdk1.8.0libct.sym(META-INF/sym/rt.jar/java/io/Closeable.class)]]
[正在加载ZipFileIndexFileObject[D:Program FilesJavajdk1.8.0libct.sym(META-INF/sym/rt.jar/java/io/FilterOutputStream.class)]]
[正在加载ZipFileIndexFileObject[D:Program FilesJavajdk1.8.0libct.sym(META-INF/sym/rt.jar/java/io/OutputStream.class)]]
[正在加载ZipFileIndexFileObject[D:Program FilesJavajdk1.8.0libct.sym(META-INF/sym/rt.jar/java/io/Flushable.class)]]
[正在加载ZipFileIndexFileObject[D:Program FilesJavajdk1.8.0libct.sym(META-INF/sym/rt.jar/java/lang/Comparable.class)]]
[正在加载ZipFileIndexFileObject[D:Program FilesJavajdk1.8.0libct.sym(META-INF/sym/rt.jar/java/lang/CharSequence.class)]]
[正在加载ZipFileIndexFileObject[D:Program FilesJavajdk1.8.0libct.sym(META-INF/sym/rt.jar/java/lang/StringBuilder.class)]]
[正在加载ZipFileIndexFileObject[D:Program FilesJavajdk1.8.0libct.sym(META-INF/sym/rt.jar/java/lang/AbstractStringBuilder.class)]]
[正在加载ZipFileIndexFileObject[D:Program FilesJavajdk1.8.0libct.sym(META-INF/sym/rt.jar/java/lang/StringBuffer.class)]]
[已写入RegularFileObject[D:projecttargetclassespackageAMain.class]]
[共 258 毫秒]
  • -verbose:输出有关编译器正在执行的操作的消息
  • -encoding:指定源文件使用的字符编码
  • -classpath:指定查找用户类文件和注释处理程序的位置。classpath默认为.;%JAVA_HOME%lib;%JAVA_HOME%libtools.jar
  • 编译顺序:先编译A.java,再编译使用到AMain.java

通过日志我们可以清晰的看到,java程序编译时,到指定的classpath路径下搜索用到的源文件和类文件,然后找到依赖的class文件并引用,最终在指定的targetclasses目录生成了对应的class文件。

targetclasses目录我们运行程序:
运行时java的类加载器会将class文件加载进去,然后进行链接、初始化,最终执行

 java -classpath "D:projectlibspring-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?

文章来源于互联网,如有雷同请联系站长删除:【Java转Go】弄清GOPATH

发表评论