Golang中使用文件夹管理组件的最佳实践
Golang中使用文件夹管理组件的最佳实践 你好,经过一段时间,我的一个项目变得越来越大,我把一些东西放到了库中,但随后意识到我在处理大小写时遇到了困难(重构时,aaa => Aaa, Aaa => aaa)。如果我全部大写,我的命名风格就会受损,感觉完全不对(尽管这可能是一个选项)。如果我不大写,我就无法创建那些库。
所以,没问题,只要决定什么是库的一部分,什么不是。这样我就不用不断地更改名称的首字母了。但我发现,不可能决定什么是库的一部分,什么不是,因为放在那里的任何东西都是一个组件,而不是一个库(包)。
那些变大的东西可能是一个组件,但不是库。为组件使用文件夹看起来确实不错。
因此,既然文件夹也比使用前缀更好(例如,我发现使用 gfx\sprite.go 比使用 gfx_sprite.go 以及所有其他文件如 snd_explosion.go 等更容易处理),我决定修改我的构建脚本,使其在使用 go 之前将所有文件复制到一个临时文件夹中。瞧,我终于可以使用文件夹了。编辑器会重写路径——没问题。
但是复制需要一些时间,而且在编辑器中使用构建脚本比以前更卡顿。所以,移动文件(使用移动命令而不是复制命令)可能是一个选项。那样很快。在 Windows 上,我不知道可以创建什么样的符号链接来实现类似的效果。另一个选项:直接买一台更快的电脑。
或者也许有另一种方法?也许……如果 Go 允许我做点什么?也许……有一天它会允许我使用子文件夹中的文件?
此致
附言:我想到的是“库”,而在 Go 中,对应的术语是“包”。
更多关于Golang中使用文件夹管理组件的最佳实践的实战教程也可以访问 https://www.itying.com/category-94-b0.html
你好,欢迎。
Go语言关于使用其他包中的元素有一条简单的规则:你需要将符号(类型、函数、变量等)“导出”,而要将一个符号“导出”,你必须将其名称首字母大写。
如果你将某些内容移动到单独的包中,你“仅仅”需要重命名那些应该在包外部可用的符号(并非所有符号)。
如果我大写所有内容,我的命名风格会受到影响,感觉完全不对。
你能详细说明一下这一点吗?
更多关于Golang中使用文件夹管理组件的最佳实践的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
所以,在 Windows 中创建硬链接的方法是 mklink /h source target。虽然使用与编辑时不同的路径进行编译带来的复杂性不会消失,但速度更快。除非它变得太令人沮丧,否则我会坚持使用这个方法。
创建包——也许可以,但前提是我知道之后不会改变结构。为了防止出现糟糕的布局,我会按照 Dean_Davidson 建议的那样做。
谢谢
是否有人同意,大小写/数据隐藏规则是 Go 语言最大的设计缺陷?
我曾以为会是其他问题,比如过度简化的错误报告行为和消息、不保证任何结构体布局、即使有类型参数泛型也显得有些弱——但在我看来,无法在(将代码从主包移动到库包,从库包移动到主包 - 或者不得不一直使用大写字母)之间进行重构才是这门语言最大的缺陷。不过,谁在乎我的想法呢,对吧?我一定是做错了什么,唉。实际上,Go 公开发布时我就尝试过,所以至少对它进行了一些测试,但我没想到对于稍大一点的项目(仅仅几 MB 的 Go 代码),这会是头号生产力杀手。
到目前为止,我还没有找到任何好的解决方案。目前,我干脆选择了复制代码,这当然不是什么好方法。
alex1:
我通常一开始不知道应该把东西放在哪里。
听起来你可能需要仔细思考一下你的包设计和公开接口。我的文件夹/包结构通常是自然生长的。比如——最近我有一个项目,需要集成Slack,所以我创建了一个 slack 包来抽象这部分功能。然后我需要集成GitHub;同样处理。我的每个包都只做一件事,从文件夹结构就能清楚地看出它们的作用。
alex1:
这也将使我免于在决定它们再次属于主包时去修改名称的大小写——毕竟那些大写字母应该用来指示你正在处理什么,而不仅仅是为了让编译器停止报错。
如果你想要有导出的类型/函数,但又担心其他包导入它们,你可以看看 internal 包:
使用内部包来减少你的公共API表面积 | Dave Cheney
总的来说,我认为如果你在创建包之前就考虑好包的命名和职责分离,你的大部分问题都会消失。这里有两篇关于包命名的优秀文章,可能会对你有帮助(我认为第一篇是Go开发者必读的!):

如何命名你的包。
Dean_Davidson:
如果你想导出类型/函数,但又担心其他包导入它们,你可以看看
internal包机制:
这个特性似乎为两人或更多员工的团队提供了一个解决方案,其中一名员工(或雇主)因为其他员工对软件缺乏了解而需要隐藏/保护代码。
我欣赏所有关于企业、面向对象、敏捷等等为特性辩护的理由,并且也乐于尝试不少。也许在某个时候,我甚至可能会喜欢 internal 包,也许是为了保护代码不受我自己的干扰,谁知道呢。
但我真正觉得 Go 语言中缺少的,是对那些处于创造性设计过程中的人的认可,他们用代码迭代(思考、冲刺等等)这个过程,而不是在纸上——也不是在谷歌,不使用服务器或大量员工。我认为,当最好的语言仍然是错误的选择时,也许只是运气不好。必须找到一个好的变通方法;我曾想过修改编辑器,使其更加了解 Go 语法,这似乎是个好主意,但我也在想:既然你需要开始进行代码检查,也就是实际上部分重写编译过程,那为什么一开始还要使用这种语言呢?当这只是一个变通方法时,为什么要费心处理这些麻烦事。
附言:有趣的是,最近我在使用 Goland。这个故事与我的包问题关系不大,但和错误报告有关。情况是这样的:你写代码,添加导入。然后你注释掉一行,Goland 会做它聪明的事情,移除导入,否则文件将无法编译。这是 Goland 的一个不错的功能。现在你又在代码中取消注释,Goland 就不知道该怎么办了。你回到文件开头,再次添加导入,这功能真棒。 😉
在Go中,包(package)是代码组织的基本单元,每个目录对应一个包。根据你的描述,核心问题是如何在保持命名风格的同时,有效管理大型项目中的组件化代码。以下是几种专业解决方案:
1. 使用内部包(internal package)
Go的internal目录机制允许你在项目内部创建私有包,这些包只能被父目录及其子目录导入。
项目结构示例:
project/
├── cmd/
│ └── main.go
├── internal/
│ ├── gfx/
│ │ ├── sprite.go
│ │ └── texture.go
│ └── snd/
│ ├── explosion.go
│ └── music.go
├── pkg/
│ └── public/
│ └── api.go
└── go.mod
internal/gfx/sprite.go:
package gfx
import "fmt"
type Sprite struct {
X, Y float64
Image string
}
func (s *Sprite) Draw() {
fmt.Printf("Drawing sprite at (%v, %v)\n", s.X, s.Y)
}
cmd/main.go:
package main
import (
"project/internal/gfx"
"project/internal/snd"
)
func main() {
sprite := &gfx.Sprite{X: 100, Y: 200, Image: "player.png"}
sprite.Draw()
sound := snd.NewExplosion()
sound.Play()
}
2. 使用多模块工作区(Go 1.18+)
Go 1.18引入了工作区功能,允许在单个工作区中处理多个模块。
go.work文件:
go 1.18
use (
./components/gfx
./components/snd
./mainapp
)
项目结构:
workspace/
├── go.work
├── components/
│ ├── gfx/
│ │ ├── go.mod
│ │ └── sprite.go
│ └── snd/
│ ├── go.mod
│ └── explosion.go
└── mainapp/
├── go.mod
└── main.go
components/gfx/sprite.go:
package gfx
// 保持小写命名风格
type spriteManager struct {
sprites []Sprite
}
func NewManager() *spriteManager {
return &spriteManager{}
}
3. 使用构建标签(build tags)
对于平台特定的代码或可选组件,可以使用构建标签。
项目结构:
project/
├── components/
│ ├── gfx.go
│ ├── gfx_directx.go // +build windows
│ ├── gfx_opengl.go // +build !windows
│ └── snd.go
└── main.go
components/gfx.go:
//go:build !windows && !darwin
// +build !windows,!darwin
package components
type Renderer interface {
Draw()
}
var defaultRenderer Renderer = &softwareRenderer{}
components/gfx_directx.go:
//go:build windows
// +build windows
package components
type directXRenderer struct{}
func (d *directXRenderer) Draw() {
// DirectX实现
}
func init() {
defaultRenderer = &directXRenderer{}
}
4. 使用代码生成工具
对于需要跨包共享的类型,可以使用代码生成工具如stringer或自定义生成器。
tools/tools.go:
//go:build tools
// +build tools
package tools
import _ "golang.org/x/tools/go/packages"
生成组件注册代码:
// 使用go:generate指令
//go:generate go run gencomponents.go
package main
// 生成的代码会被放在generated/目录下
import _ "project/generated/components"
5. 使用接口和依赖注入
通过接口抽象组件,使用依赖注入管理组件生命周期。
component/registry.go:
package component
type Registry struct {
components map[string]interface{}
}
func (r *Registry) Register(name string, comp interface{}) {
r.components[name] = comp
}
func (r *Registry) Get(name string) interface{} {
return r.components[name]
}
使用示例:
package main
import (
"project/component"
"project/components/gfx"
"project/components/snd"
)
func main() {
registry := &component.Registry{}
// 注册组件
registry.Register("gfx", gfx.NewManager())
registry.Register("snd", snd.NewManager())
// 获取组件
gfxComp := registry.Get("gfx").(gfx.Manager)
sndComp := registry.Get("snd").(snd.Manager)
}
实际项目结构建议
myproject/
├── cmd/
│ ├── app1/
│ │ └── main.go
│ └── app2/
│ └── main.go
├── internal/
│ ├── component/
│ │ ├── registry.go
│ │ └── interface.go
│ ├── gfx/
│ │ ├── sprite.go
│ │ ├── texture.go
│ │ └── manager.go
│ └── snd/
│ ├── explosion.go
│ ├── music.go
│ └── manager.go
├── pkg/
│ ├── publicapi/
│ │ └── client.go
│ └── utils/
│ └── helpers.go
├── go.mod
└── go.sum
这种结构允许你:
- 保持小写命名风格
- 组件化组织代码
- 避免命名冲突
- 保持导入路径清晰
- 利用Go的包可见性规则
Go的包系统设计鼓励扁平结构,但对于大型项目,合理使用internal目录和多模块工作区可以有效管理组件化代码,无需依赖文件复制或符号链接等变通方案。


