基于Clean Architecture的Go项目架构实践

经过这些年的发展,Go 语言已经成为一门被广泛使用在各个领域的编程语言。从 k8s、docker 等基础组件,到业务领域的微服务,都可以用 Go 构建。在构建这些 Go 项目时,采用哪种架构模式和代码布局,是一个仁者见仁智者见智的事情。有 Java Spring 经验的可能会采用 MVC 模式,有 Python Flask 经验的可能会采用 MTV 模式。加上 Go 语言领域并没有出现主流的企业级开发框架,很多项目甚至没有明确的架构模式。 Clean Architecture Clean Architecture 是 Uncle Bob 提出的适用于复杂业务系统的架构模式,其核心思想是将业务复杂度与技术复杂度解藕,相比于 MVC、MTV 等模式,Clean Architecture 除了进行分层,还通过约定依赖原则,明确了与外部依赖的交互方式,以及外部依赖与业务逻辑的边界。感兴趣的朋友可以直接阅读作者原文https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html。 由于 Clean Architecture 具有脱离语言和框架的灵活性,作者在提出时也没有规定实现细节,给 Clean Architecture 的落地带来了困难,接下来以一个例子来说明如何在 Go 项目中应用 Clean Architecture 的思想。 布局 作为一个 Go 项目,不管用哪种架构模式,建议都建立 app 和 scripts 这两个路径。app 存放启动 Go 项目的入口文件,通常是 main.go。而 scripts 可以放一些构建和部署时候用到的脚本。 clean_architecture_demo ├── README.md ├── app │ └── main.go ├── scripts │ ├── build.sh │ └── run.sh ├── go.mod ├── go.sum └── usecases 接下来是代码部分,分为 entities、usecases、adapters 三个部分。 ...

十二月 12, 2021 · 1 分钟 · Zhiya

规避 Go 中的常见并发 bug

在Understanding Real-World Concurrency Bugs in Go这篇论文中,几名研究人员分析了常见的Go并发bug,并在最流行的几个Go开源项目中进行了验证。本文梳理了论文中提到的常见的bug并给出解决方法的分析。 论文中对bugs进行了分类,分为阻塞式和非阻塞式两种: 阻塞式:goroutine发生阻塞无法继续执行(例如死锁) 非阻塞式:不会阻塞执行,但存在潜在的数据冲突(例如并发写) 阻塞式bug 阻塞式bug发生的根因有两种,一种是共享内存(例如卡在了意图保护共享内存的锁操作上),一种是消息传递(比如等待chan)。同时研究发现共享内存和消息传递导致的bug数量不想上下,但是共享这种方法的使用量比消息传递使用的更频繁,所以也得出了共享内存方式更不容易导致bug的结论。 读写锁优先级导致的死锁 在Go中的写锁优先级高于读锁优先级,假设一个goroutine(goroutine A)连续获取两次读锁,而另一个goroutine(goroutine B)在gouroutine A两次获取读锁中间获取了写锁,就会导致死锁的发生。论文中没有针对这个bug给出示例代码,我写了一个简单的代码示意一下。 func gouroutine1() { m.RLock() m.RLock() } func gouroutine2() { m.WLock() } f1和f2都在goroutine中执行,当f1执行完第一个l.RLock()语句后,假设这时f2的m.WLock执行,由于写锁是排它的,WLock本身被f1的第一个m.RLock()阻塞,写锁操作本身又会阻塞f1中的第二个m.RLock WaitGroup误用导致的死锁 这种情况就是比较典型的WaitGroup的误用了,提前执行group.Wait()会导致部分group.Done()无法执行到,进而导致程序被阻塞。 var group sync.WaitGroup group.Add(len(pm.plugins)) for _, p := range pm.plugins { go func(p *plugin) { defer group.Done() } group.Wait() // blocked } // group.Wait() should be here for循环内的group.Wait()执行到的时候,循环内的部分goroutine还没有被创建出来,其中的group.Done()也就永远没法执行到,所以会导致永远阻塞在这一句,正确的写法是将group.Wait()移到for循环外。 Channel的误用 Channel是go支持并发的一个非常重要的特性,Channel虽然在很多场景下非常解决问题,但是误用也是不容易发现的。 func goroutine1() { m.Lock() ch <- request // blocked m.Unlock() } func goroutine2() { for { m.Lock() // 阻塞 m.Unlock() request <- ch } } 这段代码的业务语义是goroutine1会通过ch接收goroutine2发送的消息,但是当goroutine1执行到ch <- request时候会阻塞并等待ch,此时由于goroutine1没有释放锁,goroutine2的m.Lock()也会阻塞,形成死锁。 ...

八月 17, 2021 · 2 分钟 · Zhiya

探究vscode debug流程,解决无法运行go程序的问题

问题描述 vscode 无法以 run 模式运行 go 项目(只能以 debug 模式调试),并且有如下报错。 图中被遮盖的部分是项目内的 package,并非第三方 package,也就是说在以 run 模式运行 go 项目时无法找到其他的 go 文件,只能找到入口文件。 初步排查 找不到其他文件,首先想到的是 GO_PATH 的问题,但是项目使用了 go mod,允许在 GO_PATH 之外的路径创建项目,所以这个怀疑点排除。接下来怀疑 vscode 的配置有问题,每个 vscode 项目中都有 .launch.json 文件,配置运行代码时的环境,下面是项目中的 .launch.json。 { // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "name": "Launch", "type": "go", "request": "launch", "mode": "auto", "program": "${workspaceRoot}/src/main.go", "env": {}, "args": [] } ] } 可以看到 .launch.json 里没有指定程序的工作目录,debug 模式和 run 模式会不会默认的工作路径不同呢?于是在 main 函数里使用 os.Getwd() 打印一下当前的路径,结果如下: ...

四月 20, 2020 · 2 分钟 · Zhiya

viper从etcd读取配置失败的问题

问题描述 Viper (本文环境是 Viper 1.1.0)是 Go 应用程序的完整配置解决方案,在很多项目中都有应用。etcd是一个分布式 KV 存储,最直接的应用是配置中心。 Viper 除了支持从文件中读取配置,还支持从远程的配置中心读取配置,使用下面的代码进行配置。 viper.AddRemoteProvider("etcd", "http://127.0.0.1:2379", "conf.toml") viper.SetConfigType("toml") err := viper.ReadRemoteConfig() if err != nil { panic(err) } 运行后报错panic: Remote Configurations Error: No Files Found,检查后发现 etcd 开启了 tls,所以需要用 https 协议访问 etcd 的 API,更新代码如下。 viper.AddSecureRemoteProvider("etcd", "https://127.0.0.1:2379", "conf.toml", "key_path") viper.SetConfigType("toml") err := viper.ReadRemoteConfig() if err != nil { panic(err) } 使用AddSecureRemoteProvider方法替换AddRemoteProvider方法,问题依旧。 定位问题 跟踪源码发现,最终像 etcd 发送请求的是go-etcd包(目前 go-etcd 已经不维护),在 go-etcd 的 requests.go 文件中找到了相关的源码,go-etcd 调用了 net/http 包向 etcd 发送请求。 这个时候忽然想到 etcd 的证书是自签名的,访问自签名证书的 https 接口应该会报错啊,怎么会请求到内容呢?如下图,在 Chrome 中访问 etcd 的自签名 https 接口,会提示证书无效。 ...

四月 16, 2020 · 1 分钟 · Zhiya

go json 实践中遇到的坑

在使用 go 语言开发过程中,经常需要使用到 json 包来进行 json 和 struct 的互相转换,在使用过程中,遇到了一些需要额外注意的地方,记录如下。 整数变浮点数问题 假设有一个 Person 结构,其中包含 Age int64 和 Weight float64 两个字段,现在通过 json 包将 Person 结构转为 map[string]interface{},代码如下。 type Person struct { Name string Age int64 Weight float64 } func main() { person := Person{ Name: "Wang Wu", Age: 30, Weight: 150.07, } jsonBytes, _ := json.Marshal(person) fmt.Println(string(jsonBytes)) var personFromJSON interface{} json.Unmarshal(jsonBytes, &personFromJSON) r := personFromJSON.(map[string]interface{}) } 代码执行到这里看上去一切正常,但是打印一下 map[string]interface{} 就会发现不太对了。 fmt.Println(reflect.TypeOf(r["Age"]).Name()) // float64 fmt.Println(reflect.TypeOf(r["Weight"]).Name()) // float64 转换成 map[string]interface{} 之后,原先的 uint64 和 float64 类型都被转换成了 float64 类型,这显然是不符合我们的预期的。 ...

十二月 24, 2018 · 2 分钟 · Zhiya