当前位置: 首页 > 科技观察

Go命令行工具项目结构最佳实践

时间:2023-03-13 01:50:34 科技观察

最近在重构一个早期实现的命令行工具项目。在修改项目结构的过程中,没有看到Go语言项目结构的最佳实践。其他语言都有这样的推荐项目结构。比如Java有一个典型的项目结构,开发者可以在这个项目结构下进行开发。Python使用Django和Flask时,框架会直接定义项目结构。但Go社区实际上尚未就项目结构达成一致。虽然已经有一些推荐的项目结构,但是一般推荐的结构并不适合我的项目。本文探讨了我的项目的最终结构,并将其与标准最佳实践进行了比较。使用良好的包设计构建项目第一个最佳实践是将项目中的任何可重用代码作为包。如何设计包结构和包的最佳时间需要单独写。我已经分享过一次这个内容。ppt链接如下:https://go-pkg-structure.dev/将代码打成一个A包比仅仅复用代码要有益得多。从项目结构来看,将代码放入独立的包中,有助于将功能独立的代码进行分组,方便其他参与开发的开发人员维护代码,对开源项目具有重要意义。独立于单个包的做法可以使项目更易于测试。将函数分离到单独的包中,这样每个函数都可以在更少的依赖项下进行测试。在创建和重构项目时,我做的第一件事就是编写项目所需的包,甚至在编写代码之前创建基本的项目结构。将应用程序逻辑与访问层逻辑分开我看到的另一个经常使用的最佳实践是将应用程序代码与访问层代码分开,其中访问代码指的是主包和main()方法。Go和其他语言一样。应用的接入层代码是main方法。当应用程序开始运行时,它是要执行的逻辑的第一部分。很可能所有的初始化逻辑都只写在main方法中。最好在app包中实现各自的初始化逻辑,而不是全部写在main方法中。初始化逻辑最好放在各自的包中,这样也更方便测试。比如将Start()、Stop()、Shutdown()方法放在app包中,编写测试代码时就可以调用当前包中的start和stop函数。下面是应用包中的一个实现示例:如果你的命令行工具项目既有服务端代码也有客户端代码,那么在一个app文件夹中实现的逻辑可以被服务端和客户端共享。但是,这种方式对于简单的命令行应用程序并不友好,可能会处于启动-执行-停止模式。但是我还是选择使用将逻辑放在app目录下的方式,这样可以把运行时逻辑放在一起,降低其他开发者理解这个项目的难度。什么应该放在主包中?把我们所有的应用都放到app包里之后,还要考虑一下main包里有什么。很简单,主包里面的内容很少。一般来说,我会将主包限制为“仅与用户交互的代码”。比如我的项目有cli和server端逻辑,我一般会把命令行参数解析的逻辑放在main包里。服务器和客户端cli编译的二进制文件将包含不同的包。通过解析主包中的参数,可以为不同的clis创建独立的选项。其他需要与用户交互的命令行应用,我也倾向于放在main包中,例如:解析用户输入的命令行参数(很简单的输入,不涉及核心逻辑)解析配置文件退出逻辑处理信号下面的代码是一个main方法的例子://mainrunsthecommand-lineparsingandvalidations.Thisfunctionwillalsostarttheapplicationlogicexecution.funcmain(){//Parsecommand-lineargumentsvaroptsoptionsargs,err:=flags.ParseArgs(&opts,os.Args[1:])iferr!=nil{os.Exit(1)}//Converttointernalconfigcfg:=config.New()cfg.Verbose=opts.Verbose//moretakingcommandlineoptionsandputtingthemintoaconfigstruct.ifopts.Pass{//asktheuserforapassword}//RuntheApperr=app.Run(cfg)iferr!=nil{//dostuffos.Exit(1)}}一个推荐的项目结构推荐很多的项目结构如下:internal/app-核心应用程序功能仅供内部使用internal/pkg/-packagepkg/仅供内部使用-Packagecmd/需要与外部代码共享的-将主包与应用程序名称放在该目录中。这个推荐结构的要点之一就是把核心代码放在internal/app,入口代码放在cmd/。这种结构对于一次编译多个二进制文件的项目非常友好,比如一次编译server和cli的项目。cmd/应该包含cli的main方法,cmd/-server目录应该包含服务器的main方法。两者可以共享internal/app目录下的其他代码。总的来说,这也是一个不错的目录结构,但是这个目录结构对我不适用,看看我是怎么改的。我把包放在其他路径下了。我所做的和上一段中推荐的结构之间的区别是包的路径。应用项目结构子目录太多,和standalone项目结构不同,我不喜欢用应用项目结构组织代码。在我看来,太多的子目录会阻碍开发人员找到功能实现的代码。子目录很多,对于代码量大的重量级项目可能是必须的,但是对于中小型项目最好不要使用这种项目结构。我选择把所有的包都放在代码的根目录下。比如我有一个Parser包,路径是parser/,ssh包的路径是ssh/,app包的路径是app/。这种方法可以很容易地找到包和函数,因为包和代码位于项目的第一层。还是那句话,把所有包都放在一级目录的做法,适合包比较少的项目。如果项目包变多了,把包放在pkg/路径下还是靠谱的。我没有采用internal和pkg模式。我觉得把代码放到internal/或者pkg/里不太好。主要原因是这种做法是针对app内部包的。但是,对于应用程序的内部包,并没有明确的“内”和“外”之分。对于仅供内部使用的包,许多开发人员根本不使用最佳实践,因为“没有其他人使用这些包”。我也不希望开发者像维护独立项目一样维护pkg路径下的代码。在实际开发中,这些包中的接口可能会像独立项目一样发生变化,所以如果真的将这些逻辑一一分开,还是在独立项目中实现比较好。将所有项目的内部代码放在同一个文件夹中对我来说更有意义。在顶级目录或pkg/目录中。我没有将所有文件放入cmd/目录cmd/目录不适用于我的项目。我在这个项目中有一个简单的CLI应用程序。该应用程序应该方便用户下载和安装。最快最方便的安装方式是使用goget命令安装:$goget-ugithub.com/madflojo/efs2我希望用户只使用goget添加项目url来安装,但是如果你使用cmd目录,需要让用户在url中添加/cmd/进行安装:$goget-ugithub.com/madflojo/efs2/cmd/efs2url格式比较乱,用户需要知道我的项目结构如何是在安装之前。我希望项目结构让其他人更容易而不是更麻烦。所以我把这个小应用的main.go文件放在了项目的顶层文件夹,方便用户直接通过goget命令安装应用,把应用的功能实现放在app包中。本文模式应用结果总结如下:$tree-L2.├──CONTRIBUTING.md├──Dockerfile├──LICENSE├──Makefile├──README.md├──app│├──app.go│└──app_test.go├──config│├──config.go│└──config_test.go├──dev-compose.yml├──go.mod├──go.sum├──main.go├──parser│├──parser.go│└──parser_test.go├──ssh│├──ssh.go│└──ssh_test.go└──供应商├──github。com├──golang。org└──modules.txt7directories,18files总的来说,我对这个结构很满意,新开发者也能比较快的上手代码。这个结构基本上看目录就知道里面的功能了。将代码函数一点一点地拆分到不同的包中也帮助我提高了代码覆盖率。目前,将主包放在顶层目录的做法还没有发现有什么坏处。当然,这种项目结构未必适合所有人,在项目开发中还是需要因地制宜。