首页 > 其他分享 >go build使用及实现

go build使用及实现

时间:2025-01-07 18:11:01浏览次数:5  
标签:package 实现 cmd tsecer pkg go build

intro

go作为一个新生的语言,跟C++相比提供了更多的易用性,但是对(习惯了C++的)新手来说这种便利也封装了更多的细节。一个基本的问题是:C++工程通常基于Makefile/CMake/bazel等外部工具进行构建,但是go的构建通常只需要使用go build或者go install这样的单个命令完成即可。

这也也会带来一些疑惑:当一个工程会生成多个命令(例如k8s输出的kubectl、kube apiserver等)的时候,go build如何来构建?如何区分构建输出的是一个package还是一个可执行文件?命令行中可以同时提供package(文件夹)和go源文件吗?可以同时构建多个package吗?命令行中的文件夹会自动递归处理吗?

由于文件夹和package有某种天然的相似性,那么同一个文件夹中可以包含多个package吗?反过来,一个package可以分散在多个文件夹吗?

同时包含源文件和文件

当命令行同时提供了源文件(p1.go)和文件夹(.)时,build会提示错误:named files must be .go files: .

(gdb) info proc
process 309746
cmdline = '/home/tsecer/source/go/bin/go build p1/p1.go .'
cwd = '/home/tsecer/playground/go/multiple_pack'
exe = '/home/tsecer/source/go/bin/go'
(gdb) bt
#0  cmd/go/internal/load.GoFilesPackage (ctx=..., opts=..., gofiles=..., ~r0=<optimized out>) at /home/tsecer/source/go/src/cmd/go/internal/load/pkg.go:3120
#1  0x00000000008c5d5c in cmd/go/internal/load.PackagesAndErrors (ctx=..., opts=..., patterns=..., ~r0=..., ~r0=...) at /home/tsecer/source/go/src/cmd/go/internal/load/pkg.go:2792
#2  0x0000000000928ce8 in cmd/go/internal/work.runBuild (ctx=..., cmd=<optimized out>, args=...) at /home/tsecer/source/go/src/cmd/go/internal/work/build.go:470
#3  0x00000000009c282e in main.invoke (cmd=0xf92be0, args=...) at /home/tsecer/source/go/src/cmd/go/main.go:295
#4  0x00000000009c1c79 in main.main () at /home/tsecer/source/go/src/cmd/go/main.go:209
(gdb) 

通过对应的代码和调用链可以看到:如果在命令行中检测到一个".go"结尾的文件,则会进入到GoFilesPackage函数。

///@file: src\cmd\go\internal\load\pkg.go
func PackagesAndErrors(ctx context.Context, opts PackageOpts, patterns []string) []*Package {
	ctx, span := trace.StartSpan(ctx, "load.PackagesAndErrors")
	defer span.Done()

	for _, p := range patterns {
		// Listing is only supported with all patterns referring to either:
		// - Files that are part of the same directory.
		// - Explicit package paths or patterns.
		if strings.HasSuffix(p, ".go") {
			// We need to test whether the path is an actual Go file and not a
			// package path or pattern ending in '.go' (see golang.org/issue/34653).
			if fi, err := fsys.Stat(p); err == nil && !fi.IsDir() {
				pkgs := []*Package{GoFilesPackage(ctx, opts, patterns)}
				setPGOProfilePath(pkgs)
				return pkgs
			}
		}
	}
///...
}

GoFilesPackage函数中会检测所有输入都为".go"结尾(的源文件)。

///@file: src\cmd\go\internal\load\pkg.go
func GoFilesPackage(ctx context.Context, opts PackageOpts, gofiles []string) *Package {
	modload.Init()

	for _, f := range gofiles {
		if !strings.HasSuffix(f, ".go") {
			pkg := new(Package)
			pkg.Internal.Local = true
			pkg.Internal.CmdlineFiles = true
			pkg.Name = f
			pkg.Error = &PackageError{
				Err: fmt.Errorf("named files must be .go files: %s", pkg.Name),
			}
			pkg.Incomplete = true
			return pkg
		}
	}
	///..
}

结论:build的输入如果包含一个go源文件,所有输入都必须是源文件。

文件夹

开始的分支就是判断是否开启了module功能,这个功能直观上说就是go.mod实现的功能。如果没有开启该功能(cfg.ModulesEnabled),会执行到search.ImportPaths函数,从这个名字看,应该主要是文件系统的操作。

///@file: src\cmd\go\internal\load\pkg.go
func PackagesAndErrors(ctx context.Context, opts PackageOpts, patterns []string) []*Package {
	ctx, span := trace.StartSpan(ctx, "load.PackagesAndErrors")
	defer span.Done()
///...
	var matches []*search.Match
	if modload.Init(); cfg.ModulesEnabled {
		modOpts := modload.PackageOpts{
			ResolveMissingImports: true,
			LoadTests:             opts.ModResolveTests,
			SilencePackageErrors:  true,
		}
		matches, _ = modload.LoadPackages(ctx, modOpts, patterns...)
	} else {
		noModRoots := []string{}
		matches = search.ImportPaths(patterns, noModRoots)
	}
///...
}

search.ImportPaths返回的是一个Match结构,从结构中看,函数已经将文件系统中内容按照package提取了出来(也就是从文件系统文件转换成了对应的package名字)。

///@file: src\cmd\go\internal\search\search.go
// A Match represents the result of matching a single package pattern.
type Match struct {
	pattern string   // the pattern itself
	Dirs    []string // if the pattern is local, directories that potentially contain matching packages
	Pkgs    []string // matching packages (import paths)
	Errs    []error  // errors matching the patterns to packages, NOT errors loading those packages

	// Errs may be non-empty even if len(Pkgs) > 0, indicating that some matching
	// packages could be located but results may be incomplete.
	// If len(Pkgs) == 0 && len(Errs) == 0, the pattern is well-formed but did not
	// match any packages.
}

文件夹搜索完成之后,对于package和main的处理主要还是在runBuild函数,这个函数比较有意思的地方是递归调用了构建进程(go build),这也意味着它能够处理多个package的情况。

这里的流程是判断如果命令行通过-o选项指定了输出,如果输出是一个文件夹,那么循环中会为每个main生成一个AutoAction动作(进行构建);如果没有指定-o输出,则会对所有搜索到的pkg调用"go build"进程。

///@file: src\cmd\go\internal\work\build.go
func runBuild(ctx context.Context, cmd *base.Command, args []string) {
///...
	if cfg.BuildO != "" {
		// If the -o name exists and is a directory or
		// ends with a slash or backslash, then
		// write all main packages to that directory.
		// Otherwise require only a single package be built.
		if fi, err := os.Stat(cfg.BuildO); (err == nil && fi.IsDir()) ||
			strings.HasSuffix(cfg.BuildO, "/") ||
			strings.HasSuffix(cfg.BuildO, string(os.PathSeparator)) {
			if !explicitO {
				base.Fatalf("go: build output %q already exists and is a directory", cfg.BuildO)
			}
			a := &Action{Mode: "go build"}
			for _, p := range pkgs {
				if p.Name != "main" {
					continue
				}

				p.Target = filepath.Join(cfg.BuildO, p.DefaultExecName())
				p.Target += cfg.ExeSuffix
				p.Stale = true
				p.StaleReason = "build -o flag in use"
				a.Deps = append(a.Deps, b.AutoAction(ModeInstall, depMode, p))
			}
			if len(a.Deps) == 0 {
				base.Fatalf("go: no main packages to build")
			}
			b.Do(ctx, a)
			return
		}
		if len(pkgs) > 1 {
			base.Fatalf("go: cannot write multiple packages to non-directory %s", cfg.BuildO)
		} else if len(pkgs) == 0 {
			base.Fatalf("no packages to build")
		}
		p := pkgs[0]
		p.Target = cfg.BuildO
		p.Stale = true // must build - not up to date
		p.StaleReason = "build -o flag in use"
		a := b.AutoAction(ModeInstall, depMode, p)
		b.Do(ctx, a)
		return
	}

	a := &Action{Mode: "go build"}
	for _, p := range pkgs {
		a.Deps = append(a.Deps, b.AutoAction(ModeBuild, depMode, p))
	}
	if cfg.BuildBuildmode == "shared" {
		a = b.buildmodeShared(ModeBuild, depMode, args, pkgs, a)
	}
	b.Do(ctx, a)
}

compile or link

在前面的AutoAction函数中,会直接根据package的名字决定是执行编译还是链接。

///@file: src\cmd\go\internal\work\action.go
// AutoAction returns the "right" action for go build or go install of p.
func (b *Builder) AutoAction(mode, depMode BuildMode, p *load.Package) *Action {
	if p.Name == "main" {
		return b.LinkAction(mode, depMode, p)
	}
	return b.CompileAction(mode, depMode, p)
}

none

没有指定文件夹时使用当前目录。

///@file: src\cmd\go\internal\search\search.go
// CleanPatterns returns the patterns to use for the given command line. It
// canonicalizes the patterns but does not evaluate any matches. For patterns
// that are not local or absolute paths, it preserves text after '@' to avoid
// modifying version queries.
func CleanPatterns(patterns []string) []string {
	if len(patterns) == 0 {
		return []string{"."}
	}

一些测试

可以看见,package在go内部是编译成了.a类型文件。

tsecer@harry-LC0:~/playground/go/multiple_pack$ strace -f -s99999 -e trace=fork,execve go build  ./p1 ./p2
execve("/usr/local/go/bin/go", ["go", "build", "./p1", "./p2"], 0x7ffda83e86d8 /* 49 vars */) = 0
strace: Process 310589 attached
strace: Process 310591 attached
strace: Process 310590 attached
...
strace: Process 310614 attached
[pid 310615] execve("/usr/local/go/pkg/tool/linux_amd64/compile", ["/usr/local/go/pkg/tool/linux_amd64/compile", "-o", "/tmp/go-build1404442041/b001/_pkg_.a", "-trimpath", "/tmp/go-build1404442041/b001=>", "-p", "_/home/tsecer/playground/go/multiple_pack/p1", "-lang=go1.23", "-complete", "-buildid", "6tEaMmstUtv9GYfQK7V5/6tEaMmstUtv9GYfQK7V5", "-goversion", "go1.23.4", "-c=2", "-D", "_/home/tsecer/playground/go/multiple_pack/p1", "-importcfg", "/tmp/go-build1404442041/b001/importcfg", "-pack", "/home/tsecer/playground/go/multiple_pack/p1/p1.go"], 0xc000138008 /* 76 vars */) = 0
strace: Process 310616 attached
strace: Process 310618 attached
strace: Process 310619 attached
strace: Process 310617 attached
[pid 310614] execve("/usr/local/go/pkg/tool/linux_amd64/compile", ["/usr/local/go/pkg/tool/linux_amd64/compile", "-o", "/tmp/go-build1404442041/b055/_pkg_.a", "-trimpath", "/tmp/go-build1404442041/b055=>", "-p", "_/home/tsecer/playground/go/multiple_pack/p2", "-lang=go1.23", "-complete", "-buildid", "e22utOjUG4SFW6GL6LOZ/e22utOjUG4SFW6GL6LOZ", "-goversion", "go1.23.4", "-c=2", "-D", "_/home/tsecer/playground/go/multiple_pack/p2", "-importcfg", "/tmp/go-build1404442041/b055/importcfg", "-pack", "/home/tsecer/playground/go/multiple_pack/p2/p2.go"], 0xc000097688 /* 76 vars */ <unfinished ...>

同时构建两个可执行文件。但是也可以看到,因为package的名字是main,所以生成可执行文件的名字只能根据(第一个)文件名来决定。

tsecer@harry: date
Tue 07 Jan 2025 09:55:01 AM PST
tsecer@harry: 
tsecer@harry: 
tsecer@harry: 
tsecer@harry: date
Tue 07 Jan 2025 09:55:10 AM PST
tsecer@harry: cat p1/main.go 
package main

func main() {
}
tsecer@harry: cat p2/main.go 
package main

func main() {
}

tsecer@harry: go build -o output/ ./p1 ./p2
tsecer@harry: ll output/
total 2992
drwxrwxr-x 2 tsecer tsecer    4096 Jan  7 09:54 ./
drwxrwxr-x 5 tsecer tsecer    4096 Jan  7 09:54 ../
-rwxrwxr-x 1 tsecer tsecer 1524390 Jan  7 09:55 p1*
-rwxrwxr-x 1 tsecer tsecer 1524390 Jan  7 09:55 p2*
tsecer@harry: 

outro

go build的确相当于内置了C++中Makefile需要完成的功能,简化了构建过程。

标签:package,实现,cmd,tsecer,pkg,go,build
From: https://www.cnblogs.com/tsecer/p/18658089

相关文章

  • 【HarmonyOS应用开发——ArkTS语言】购物商城的实现【合集】
    目录......
  • AI的下一个主战场 —— “空间智能” —— 是否依然可以依靠堆算力和数据来实现呢?
    相关:3个月估值10亿,李飞飞空间智能首个模型诞生!一张图生成3D世界,视频游戏要变天Cosmos模型已经公开发布,下面是相关地址:英伟达API目录:https://build.nvidia.com/explore/simulationHuggingFace:https://huggingface.co/collections/nvidia/cosmos-6751e884dc10e013a0a0d8e6......
  • 谷歌地图案例 | Argos:利用 Google 地图平台将实体店和线上购物的优势融为一体
    关于ArgosArgos是英国零售巨头之一,每年拥有近2900万实体店顾客和超过10亿在线访客。其使命是提供卓越的便利、选择和价值。行业:零售和消费品。  GoogleCloud结果通过直观、易用的Google地图,将“跳出率”降低12%,每年留住数百万客户让顾客可以......
  • 利用matlab实现光学系统像差仿真
    利用matlab实现光学系统像差仿真,泽尼克像差代码实现列表zernike/fftprep.m , 640zernike/logim.m , 879zernike/script1.m , 1772zernike/Zernike.jpg , 12237......
  • Java Spring Boot实现基于URL + IP访问频率限制
    点击下载《JavaSpringBoot实现基于URL+IP访问频率限制(源代码)》1.引言在现代Web应用中,接口被恶意刷新或暴力请求是一种常见的攻击手段。为了保护系统资源,防止服务器过载或服务不可用,需要对接口的访问频率进行限制。本文将介绍如何使用SpringBoot实现基于URL......
  • Google Play:应用内购买与订阅服务详解_2024-07-19_07-58-06.Tex
    GooglePlay:应用内购买与订阅服务详解GooglePlay应用内购买基础1.1什么是应用内购买应用内购买(In-appPurchases)是指在应用程序或游戏中,用户可以购买额外的功能、内容或服务。这种模式允许开发者在应用免费或部分免费的基础上,通过销售虚拟商品或订阅服务来获取收入。1......
  • Google Play:应用内广告策略与AdMob集成_2024-07-19_09-20-55.Tex
    GooglePlay:应用内广告策略与AdMob集成了解应用内广告的重要性定义应用内广告应用内广告,顾名思义,是在移动应用内部展示的广告。这种广告形式允许开发者在不打断用户体验的前提下,通过应用内的广告空间赚取收入。应用内广告可以是静态的图片,也可以是动态的视频,甚至可以是互......
  • Google Play:应用商店SEO与关键词优化教程_2024-07-19_08-43-19.Tex
    GooglePlay:应用商店SEO与关键词优化教程理解GooglePlay的搜索机制搜索算法解析GooglePlay的搜索算法旨在为用户提供最相关、最有价值的应用搜索结果。它基于多种因素进行排名,包括但不限于:关键词匹配:应用的标题、描述、关键词列表中包含的关键词与用户搜索词的匹配度......
  • Is iPad good or bad for middle school students?iPad 对中学生是好还是坏?
    IsiPadgoodorbadformiddleschoolstudents?iPad对中学生是好还是坏?1写作要求假如你是李华,是一名初中生。随着科技的发展,iPad已成为中学生的新宠。用iPad既可以听音乐,阅读纯文本电子书,又可以玩电子游戏。请你用英语给某英语报社写一篇短文,谈谈中学生使用iPad的利弊。内......
  • 【编码】如何实现一套自定义网络协议?
    前言下文介绍的自定义协议仅作为学习示例,纯粹是玩具项目,没有实际可用性。无需过度关注和讨论其合理性进行通信的双方是谁?常见的模型客户端-服务器,例如HTTP协议,浏览器<=>Web服务器。中转站模型,如MQTT协议,应用服务<=>中转站<=>硬件客户端对等模型,例如Thrift协议,应用服务<=>应......