技术

Spark 内存管理及调优 Yarn学习 Spark部署模式及源码分析 容器狂占内存资源怎么办? 多角度理解一致性 golang io使用及优化模式 Flink学习 c++学习 学习ebpf go设计哲学 ceph学习 学习mesh kvm虚拟化 学习MQ go编译器 学习go 为什么要有堆栈 汇编语言 计算机组成原理 运行时和库 Prometheus client mysql 事务 mysql 事务的隔离级别 mysql 索引 坏味道 学习分布式 学习网络 学习Linux go 内存管理 golang 系统调用与阻塞处理 Goroutine 调度过程 重新认识cpu mosn有的没的 负载均衡泛谈 单元测试的新解读 《Redis核心技术与实现》笔记 《Prometheus监控实战》笔记 Prometheus 告警学习 calico源码分析 对容器云平台的理解 Prometheus 源码分析 并发的成本 基础设施优化 hashicorp raft源码学习 docker 架构 mosn细节 与微服务框架整合 Java动态代理 编程范式 并发通信模型 《网络是怎样连接的》笔记 go channel codereview gc分析 jvm 线程实现 go打包机制 go interface及反射 如何学习Kubernetes 《编译原理之美》笔记——后端部分 《编译原理之美》笔记——前端部分 Pilot MCP协议分析 go gc 内存管理玩法汇总 软件机制 istio流量管理 Pilot源码分析 golang io 学习Spring mosn源码浅析 MOSN简介 《datacenter as a computer》笔记 学习JVM Tomcat源码分析 Linux可观测性 学习存储 学计算 Gotty源码分析 kubernetes operator kaggle泰坦尼克问题实践 kubernetes扩缩容 神经网络模型优化 直觉上理解深度学习 如何学习机器学习 TIDB源码分析 什么是云原生 Alibaba Java诊断工具Arthas TIDB存储——TIKV 《Apache Kafka源码分析》——简介 netty中的线程池 guava cache 源码分析 Springboot 启动过程分析 Spring 创建Bean的年代变迁 Linux内存管理 自定义CNI IPAM 共识算法 spring redis 源码分析 kafka实践 spring kafka 源码分析 Linux进程调度 让kafka支持优先级队列 Codis源码分析 Redis源码分析 C语言学习 《趣谈Linux操作系统》笔记 docker和k8s安全访问机制 jvm crash分析 Prometheus 学习 Kubernetes监控 容器日志采集 Kubernetes 控制器模型 容器狂占资源怎么办? Kubernetes资源调度——scheduler 时序性数据库介绍及对比 influxdb入门 maven的基本概念 《Apache Kafka源码分析》——server Kubernetes类型系统 源码分析体会 《数据结构与算法之美》——算法新解 Kubernetes源码分析——controller mananger Kubernetes源码分析——apiserver Kubernetes源码分析——kubelet Kubernetes介绍 ansible学习 Kubernetes源码分析——从kubectl开始 jib源码分析之Step实现 jib源码分析之细节 线程排队 跨主机容器通信 jib源码分析及应用 为容器选择一个合适的entrypoint kubernetes yaml配置 《持续交付36讲》笔记 mybatis学习 程序猿应该知道的 无锁数据结构和算法 CNI——容器网络是如何打通的 为什么很多业务程序猿觉得数据结构和算法没用? 串一串一致性协议 当我在说PaaS时,我在说什么 《数据结构与算法之美》——数据结构笔记 PouchContainer技术分享体会 harbor学习 用groovy 来动态化你的代码 精简代码的利器——lombok 学习 《深入剖析kubernetes》笔记 编程语言那些事儿 rxjava3——背压 rxjava2——线程切换 spring cloud 初识 《深入拆解java 虚拟机》笔记 《how tomcat works》笔记 hystrix 学习 rxjava1——概念 Redis 学习 TIDB 学习 分布式计算系统的那些套路 Storm 学习 AQS1——论文学习 Unsafe Spark Stream 学习 linux vfs轮廓 《自己动手写docker》笔记 java8 实践 中本聪比特币白皮书 细读 区块链泛谈 比特币 大杂烩 总纲——如何学习分布式系统 hbase 泛谈 forkjoin 泛谈 看不见摸不着的cdn是啥 《jdk8 in action》笔记 程序猿视角看网络 bgp初识 calico学习 AQS——粗略的代码分析 我们能用反射做什么 web 跨域问题 《clean code》笔记 《Elasticsearch权威指南》笔记 mockito简介及源码分析 2017软件开发小结—— 从做功能到做系统 《Apache Kafka源码分析》——clients dns隐藏的一个坑 《mysql技术内幕》笔记 log4j学习 为什么netty比较难懂? 回溯法 apollo client源码分析及看待面向对象设计 学习并发 docker运行java项目的常见问题 OpenTSDB 入门 spring事务小结 分布式事务 javascript应用在哪里 《netty in action》读书笔记 netty对http2协议的解析 ssl证书是什么东西 http那些事 苹果APNs推送框架pushy apple 推送那些事儿 编写java框架的几大利器 java内存模型 java exception Linux IO学习 netty内存管理 测试环境docker化实践 netty在框架中的使用套路 Nginx简单使用 《Linux内核设计的艺术》小结 Go并发机制及语言层工具 Linux网络源代码学习——数据包的发送与接收 《docker源码分析》小结 docker namespace和cgroup Linux网络源代码学习——整体介绍 zookeeper三重奏 数据库的一些知识 Spark 泛谈 链式处理的那些套路 netty回顾 Thrift基本原理与实践(二) Thrift基本原理与实践(一) 回调 异步执行抽象——Executor与Future Docker0.1.0源码分析 java gc Jedis源码分析 Redis概述 深度学习泛谈 Linux网络命令操作 JTA与TCC 换个角度看待设计模式 Scala初识 向Hadoop学习NIO的使用 以新的角度看数据结构 并发控制相关的硬件与内核支持 systemd 简介 quartz 源码分析 基于docker搭建测试环境(二) spring aop 实现原理简述 自己动手写spring(八) 支持AOP 自己动手写spring(七) 类结构设计调整 分析log日志 自己动手写spring(六) 支持FactoryBean 自己动手写spring(九) 总结 自己动手写spring(五) bean的生命周期管理 自己动手写spring(四) 整合xml与注解方式 自己动手写spring(三) 支持注解方式 自己动手写spring(二) 创建一个bean工厂 自己动手写spring(一) 使用digester varnish 简单使用 关于docker image的那点事儿 基于docker搭建测试环境 分布式配置系统 JVM执行 git maven/ant/gradle/make使用 再看tcp kv系统 java nio的多线程扩展 《Concurrency Models》笔记 回头看Spring IOC IntelliJ IDEA使用 Java泛型 vagrant 使用 Go常用的一些库 Python初学 Goroutine 调度模型 虚拟网络 《程序员的自我修养》小结 Kubernetes存储 访问Kubernetes上的Service Kubernetes副本管理 Kubernetes pod 组件 Go基础 JVM类加载 硬币和扑克牌问题 LRU实现 virtualbox 使用 ThreadLocal小结 docker快速入门

架构

分布式链路追踪 helm tensorflow原理——python层分析 如何学习tensorflow 数据并行——allreduce 数据并行——ps 机器学习中的python调用c 机器学习训练框架概述 embedding的原理及实践 tensornet源码分析 大模型训练 X的生成——特征工程 tvm tensorflow原理——core层分析 模型演变 《深度学习推荐系统实战》笔记 keras 和 Estimator tensorflow分布式训练 分布式训练的一些问题 基于Volcano的弹性训练 图神经网络 pytorch弹性分布式训练 在离线业务混部 RNN pytorch分布式训练 CNN 《动手学深度学习》笔记 pytorch与线性回归 多活 volcano特性源码分析 推理服务 kubebuilder 学习 mpi 学习pytorch client-go学习 tensorflow学习 提高gpu 利用率 GPU与容器的结合 GPU入门 AI云平台 tf-operator源码分析 k8s批处理调度 喜马拉雅容器化实践 Kubernetes 实践 学习rpc BFF 生命周期管理 openkruise学习 可观察性和监控系统 基于Kubernetes选主及应用 《许式伟的架构课》笔记 Kubernetes webhook 发布平台系统设计 k8s水平扩缩容 Scheduler如何给Node打分 Scheduler扩展 controller 组件介绍 openkruise cloneset学习 controller-runtime源码分析 pv与pvc实现 csi学习 client-go源码分析 kubelet 组件分析 调度实践 Pod是如何被创建出来的? 《软件设计之美》笔记 mecha 架构学习 Kubernetes events学习及应用 CRI 资源调度泛谈 业务系统设计原则 grpc学习 元编程 以应用为中心 istio学习 下一代微服务Service Mesh 《实现领域驱动设计》笔记 serverless 泛谈 概率论 《架构整洁之道》笔记 处理复杂性 那些年追过的并发 服务器端编程 网络通信协议 架构大杂烩 如何学习架构 《反应式设计模式》笔记 项目的演化特点 反应式架构摸索 函数式编程的设计模式 服务化 ddd反模式——CRUD的败笔 研发效能平台 重新看面向对象设计 业务系统设计的一些体会 函数式编程 《左耳听风》笔记 业务程序猿眼中的微服务管理 DDD实践——CQRS 项目隔离——案例研究 《编程的本质》笔记 系统故障排查汇总及教训 平台支持类系统的几个点 代码腾挪的艺术 abtest 系统设计汇总 《从0开始学架构》笔记 初级权限系统设计 领域驱动理念入门 现有上传协议分析 移动网络下的文件上传要注意的几个问题 推送系统的几个基本问题 用户登陆 做配置中心要想好的几个基本问题 不同层面的异步 分层那些事儿 性能问题分析 当我在说模板引擎的时候,我在说什么 用户认证问题 资源的分配与回收——池 消息/任务队列

标签


go打包机制

2020年03月15日

前言

如何组织一个大项目的go 代码

宏观

$tree -F exe-layout 
exe-layout
├── cmd/
│   ├── app1/
│   │   └── main.go
│   └── app2/
│       └── main.go
├── go.mod
├── go.sum
├── internal/
│   ├── pkga/
│   │   └── pkg_a.go
│   └── pkgb/
│       └── pkg_b.go
├── pkg1/
│   └── pkg1.go
├── pkg2/
│   └── pkg2.go
└── vendor/
  1. cmd 目录就是存放项目要编译构建的可执行文件对应的 main 包的源文件
  2. pkgN 目录,这是一个存放项目自身要使用、同样也是可执行文件对应 main 包所要依赖的库文件,同时这些目录下的包还可以被外部项目引用。有的项目偏好 只有一个pkg 目录。
  3. 存放仅项目内部引用的 Go 包,这些包无法被项目之外引用;
  4. 对于以生产可复用库为目的的 Go 项目,可以在 Go 可执行程序项目的基础上去掉 cmd 目录和 vendor 目录。

具体业务

使用 Go 语言开发的一些经验(含代码示例) 要点如下

  1. 可见性和代码划分

    • c++ 在类上,即哪怕在同一个代码文件中,仍然无法访问一个类的私有方法
    • java 是 类 + 包名
    • go 在包上,从其他包中引入的常量、变量、函数、结构体以及接口,都需要加上包的前缀来进行引用。Golang 也可以 dot import 来去掉这个前缀。不幸的是,这个做法并不常规,并且不被建议。
  2. 假设有一个用户信息管理系统,直观感觉上的分包方式

    • 单一package
    • 按mvc划分,比如controller包、model包,缺点就是你使用 controller类时 就只得controller.UserController,controller 重复了
    • 按模块划分。比如user/UserControler.go,user/User.go,缺点就是使用User类时只得 user.User
  3. 按依赖划分,即根包下 定义接口文件servier.go,包含User和UserController 接口定义,然后定义postgresql/UserService.go 或者mysql/UserService.go

github 也有一些demo 项目layout golang-standards/project-layout

作为一名Java程序员,我为什么不在生产项目中转向Go

并发中处理的内容才是关键,新启一个线程或者协程才是万里长城的第一步,如果其中的业务逻辑有10个分支,还要多次访问数据库并调用远程服务,那无论用什么语言都白搭。所以在业务逻辑复杂的情况下,语言的差异并不会太明显,至少在Java和Go的对比下不明显 Organizing Go source code part 2 未读

import

import(
    // 第一部分 标准库
    // 第二部分 第三方依赖
    // 第三部分 自己的依赖
)

包管理

Golang使用包(package)这种语法元素来组织源码,所有语法可见性均定义在package这个级别,与Java 、python等语言相比,这算不上什么创新,但与C传统的include相比,则是显得“先进”了许多。参见理解Golang包导入

  编译 install
maven mvn package/compile mvn install
go go build go install

Go 依赖包管理

Go的包管理工具(一)

Go的包管理工具(二):glide

Go的包管理工具(三):Go Modules

官方对比

  版本  
vendor机制 1.5发布,1.6默认启用,1.7去掉环境变量设置默认开启  
govendor 1.5以后可用 基于 vendor 目录机制的包管理工具
godep 1.5之前可以用,1.6依赖vendor  
Go Modules 1.11 发布,1.12 增强,1.13正式默认开启  

最早的GOPATH

对于go来说,其实并不在意你的代码是内部还是外部的,总之都在GOPATH里,任何import包的路径都是从GOPATH开始的;唯一的区别,就是内部依赖的包是开发者自己写的,外部依赖的包是go get下来的。Go 语言原生包管理的缺陷:

  1. 能拉取源码的平台很有限,绝大多数依赖的是 github.com
  2. 不能区分版本(对于依赖的同一个包只能从master分支上导入最新的提交,且不能导入包的指定的版本),以至于令开发者以最后一项包名作为版本划分。
  3. 依赖 列表/关系 无法持久化到本地,需要找出所有依赖包然后一个个 go get
  4. 只能依赖本地全局仓库(GOPATH/GOROOT),无法将库放置于局部仓库($PROJECT_HOME/vendor)
  5. 所有的项目都必须在GOPATH/src指向的目录下,或者必须更改GOPATH环境变量所指向的目录。

vendor

vendor属性就是让go编译时,优先从项目源码树根目录下的vendor目录查找代码(可以理解为切了一次GOPATH),如果vendor中有,则不再去GOPATH中去查找。

通过如上vendor解决了部分问题,然而又引起了新的问题:

  1. vendor目录中依赖包没有版本信息。这样依赖包脱离了版本管理,对于升级、问题追溯,会有点困难。
  2. 如何方便的得到本项目依赖了哪些包,并方便的将其拷贝到vendor目录下?依靠人工实在不现实。

godep/govendor

在支持vendor机制之后, gopher 们把注意力都集中在如何利用 vendor 解决包依赖问题,从手工添加依赖到 vendor、手工更新依赖,到一众包依赖管理工具的诞生,比如:govendor、glide 以及号称准官方工具的 dep,努力地尝试着按照当今主流思路解决着诸如 “钻石型依赖” 等难题。

godep go build main.go godep中的go命令,就是将原先的go命令加了一层壳,执行godep go的时候,会将当前项目的workspace目录加入GOPATH变量中。

godep save命令将会自动扫描当前目录所属包中import的所有外部依赖库(非系统库),并将所有的依赖库下来下来到当前工程中,产生文件 Godeps/Godeps.json 文件。把所有依赖包代码从GOPATH路径拷贝到Godeps目录下(vendor推出后也改用vendor了)

govendor init生成vendor/vendor.json

govendor add +external更新vendor/vendor.json,并拷贝GOPATH下的代码到vendor目录中。

vendor机制有一个问题:同样的库,同样的版本,就因为在不同的工程里用了,就要在vendor里单独搞一份,不浪费吗?所有这些基于vendor的包管理工具,都会有这个问题。

Go Modules 一统天下

一个 Go Module 是一个 Go 包的集合。module 是有版本的,所以 module 下的包也就有了版本属性。这个 module 与这些包会组成一个独立的版本单元,它们一起打版本、发布和分发。

  1. repo,仓库,用来管理modules
  2. modules是打tag的最小单位,也是go mod的最小单位
  3. packages是被引用的最小单位

一文读懂Go Modules原理 手把手教你如何创建及使用Go module

版本

Go Modules 提供了统一的依赖包管理工具 go mod 基本思想semantic version(社区实际上做不到

  1. MAJOR version when you make incompatible API changes(不兼容的修改)
  2. MINOR version when you add functionality in a backwards compatible manner(特性添加,版本兼容)
  3. PATCH version when you make backwards compatible bug fixes(bug修复,版本兼容)

依赖包统一收集在 $GOPATH/pkg/mod 中进行集中管理,有点mvn .m2 文件夹的意思。$GOPATH/pkg/mod 中的按版本管理

go mod tidy 下载的依赖 module 会被放置在本地的 module 缓存路径下,默认值为 $GOPATH/pkg/mod,Go 1.15 及以后版本可以通过 GOMODCACHE 环境变量,自定义本地 module 的缓存路径,有点maven .m2 文件夹的意思。go build 命令会读取 go.mod 中的依赖及版本信息,并在本地 module 缓存路径下找到对应版本的依赖 module,执行编译和链接。

几项创新

  1. 语义导入版本:如果同一个包的新旧版本是兼容的,那么它们的包导入路径应该是相同的。如果新旧两个包不兼容,那么我们就应该采用不同的导入路径。将包主版本号引入到包导入路径中,我们可以像下面这样导入 logrus v2.0.0 版本依赖包:import "github.com/sirupsen/logrus/v2"
  2. 最小版本选择原则:主流编程语言,以及 Go Module 出现之前的很多 Go 包依赖管理工具都会选择依赖项的“最新最大 (Latest Greatest) 版本”,Go 会在该项目依赖项的所有版本中,选出符合项目整体要求的“最小版本”。

如果这个仓库下的布局是这样的:

./srsm
├── go.mod
├── go.sum
├── pkg1/
│   └── pkg1.go
└── pkg2/
    └── pkg2.go

module 的使用者可以很轻松地确定 pkg1 和 pkg2 两个包的导入路径,一个是 github.com/bigwhite/srsm/pkg1,另一个则是 github.com/bigwhite/srsm/pkg2。如果 module 演进到了 v2.x.x 版本,那么以 pkg1 包为例,它的包的导入路径就变成了 github.com/bigwhite/srsm/v2/pkg1

go.mod

$GOPATH/pkg/mod/k8s.io
    api@v0.17.0
    client-go@v0.17.0
    kube-openapi@v0.0.0-20191107075043-30be4d16710a

go.mod

  1. module:代表go模块名,也即被其它模块引用的名称,位于文件第一行
  2. require:最小需求列表(依赖模块及其版本信息)
  3. replace:通过replace将一个模块的地址转换为其它地址(开发者github 上给自己的项目换个地址,删除某个版本等,常事),用于解决某些依赖模块地址发生改变的场景。同时import命令可以无需改变(无侵入)。
  4. exclude:明确排除一些依赖包中不想导入或者有问题的版本

replace

这一次,彻底掌握go mod

  1. replace 只在 main module 里面有效。什么叫 main module? 打个比方,项目 A 的 module 用 replace 替换了本地文件,那么当项目 B 引用项目 A 后,项目 A 的 replace 会失效,此时对 replace 而言,项目 A 就是 main module。因为对于包进行替换后,通常不能保证兼容性,对于一些使用了这个包的第三方module来说可能意味着潜在的缺陷
  2. replace 指定中需要替换的包及其版本号必须出现在 require 列表中才有效。replace命令只能管理顶层依赖(无法管理间接依赖)
replace (
    golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac => github.com/golang/crypto v0.0.0-20180820150726-614d502a4dac
    golang.org/x/net v0.0.0-20180821023952-922f4815f713 => github.com/golang/net v0.0.0-20180826012351-8a410e7b638d
    golang.org/x/text v0.3.0 => github.com/golang/text v0.3.0
)

replace 的使用场景

  1. 替换无法下载的包,比如在国内访问golang.org/x的各个包都需要翻墙,你可以在go.mod中使用replace替换成github上对应的库。
  2. 替换本地自己的包
  3. 替换 fork 包,有时候我们依赖的第三方库可能有 bug,我们就可以 fork 一份他们的库,然后自己改下,然后通过 replace 将我们 fork 的替换成原来的

冲突解决(还不清晰)

如何欺骗 Go Mod ? go mod 的智障版本选择 +incompatible :如果 major version 升级至 v2 时,如果该版本没有打算向前兼容,且不想把module path添加版本后缀,则可以在build tag时以 +incompatible 结尾即可,则别的工程引用示例为 require github.com/anqiansong/foo v2.0.0+incompatible

示例

// go.mod
module A1
go 1.14
require (
	B1.2
	C1.3 
	D1.4 // indirect
	E1.3 // indirect
    X v0.0.0-20120604004816-cd527374f1e5 
)
  1. 依赖管理可以归纳为如下四个操作 ,尽量不要手动修改go.mod文件,通过go命令来操作go.mod文件
    1. 构建项目当前build list go build
    2. 升级所有依赖模块到它们的最新版本 go get -u
    3. 升级某个依赖模块到指定版本 go get C@1.3
    4. 将某个依赖模块降级到指定版本 go get D@1.2
    5. 移除某个依赖 go mod tidy。 仅从源码中删除对依赖项的导入语句不够的
  2. 通过go build编译项目时
    1. 如果在go.mod文件中指定了直接依赖模块版本,则根据最小版本选择算法会下载对应版本;
    2. 否则go build会默认自动下载直接依赖模块的最新semantic version
    3. 若没有semantic version则自动生成标签:(v0.0.0)-(提交UTC时间戳)-(commit id前12位)作为版本标识
  3. 出现indirect标记的两种情况:
    1. A1的某个依赖模块没有使用Go Modules(也即该模块没有go.mod文件),那么必须将该模块的间接依赖记录在A1的需求列表中
    2. A1对某个间接依赖模块有特殊的版本要求,必须显示指明版本信息(例如上述的D1.4和E1.3),以便Go可以正确构建依赖模块

运行go build或是go mod tidy时golang会自动更新go.mod导致某些修改无效,所以一个包是顶层依赖还是间接依赖,取决于它在本module中是否被直接import,而不是在go.mod文件中是否包含// indirect注释。

其它

浅谈如何组织Go代码结构

  1. 大多数成功的 Go 应用程序的结构并不能从一个项目复制/粘贴到另一个项目。
  2. 使用一个远比需要复杂的程序结构,实际上对项目的伤害比帮助更大。
  3. 对于一个几乎没有 Go 代码经验的人来说,发掘项目的理想结构并不是一个现实的目标。它需要实践、实验和重构来获得正确的结果。