Golang 项目结构探讨

当源代码文件多起来的时候原本方便的单层目录变得杂乱无章起来,但是 golang 又是以文件夹分包的,如果直接按照功能拆分包就会遇到变量共享的问题,参考了 gogs, beego 的代码后发现一个类似存放配置、全局变量的包变得必不可少,同时结合多年的 laravel 脚手架习惯得出下面的文件结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
├── Makefile
├── app
│   └── app.go
├── cmd
│   ├── server.go
│   └── worker.go
├── main.go
├── model
│   ├── configuration.go
│   └── participant.go
├── repository
│   ├── configuration
│   │   ├── configuration.go
│   │   └── configuration_test.go
│   └── user
│   ├── user.go
│   └── user_test.go
└── route
├── admin
│   └── admin.go
└── route.go

其中 app 包作为初始化配置以及保存全局变量而存在 app/app.go :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package app

import (
"cmn"
_ "github.com/joho/godotenv/autoload"
"github.com/go-xorm/xorm"
"fmt"
"runtime"
"strings"
"github.com/joho/godotenv"
)

var (
Debug bool
Name string
Version string
HttpPort string
DB *xorm.Engine
Log *cmn.Logger
)

const VERSION = "1.0"

func init() {
_, filename, _, _ := runtime.Caller(0)
path := strings.Split(filename, "/")
env := fmt.Sprintf("%s/.env", strings.Join(path[:len(path)-2], "/"))
godotenv.Load(env, ".env")
if debug := cmn.GetEnv("APP_DEBUG", "false"); debug == "true" {
Debug = true
}
Name = cmn.GetEnv("APP_NAME", "example")
Version = VERSION
HttpPort = cmn.GetEnv("HTTP_PORT", "8002")
// set logger
LogOption := &cmn.LogOption{
Name: "example",
Debug: Debug,
}
var err error
Log, err = cmn.NewLogger(cmn.LogAdapterStdout, LogOption)
if err != nil {
panic(err)
}

// set db
DBOption := &cmn.DBOption{
Driver: cmn.GetEnv("DB_DRIVER", "mysql"),
Host: cmn.GetEnv("DB_HOST", "127.0.0.1"),
Port: cmn.GetEnv("DB_PORT", "3306"),
Database: cmn.GetEnv("DB_DATABASE", "test"),
User: cmn.GetEnv("DB_USERNAME", "root"),
Password: cmn.GetEnv("DB_PASSWORD", ""),
Timeout: cmn.GetEnv("DB_TIMEOUT", "3s"),
Debug: Debug,
Logger: Log,
}
DB, err = cmn.NewDB(DBOption)
}

不过这里的变量并未做只读保护,如果有必要的话可以将变量改成私有,并提供 get 方法即可。

这样之后 main.go 就会显得非常简洁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"github.com/urfave/cli"
"cmn/example/cmd"
"os"
"cmn/example/app"
)

func main() {
c := cli.NewApp()
c.Name = app.Name
c.Usage = "A example project"
c.Version = app.Version
c.Commands = []cli.Command{
cmd.Server,
cmd.Worker,
}
c.Run(os.Args)
}

不过我在 example 用了 env 来初始化变量,在运行单元测试的时候因为 working directory 的改变导致没有读取项目根目录下的 .env 文件,所以用了一段 hack 代码 在 init 方法中:

1
2
3
4
_, filename, _, _ := runtime.Caller(0)
path := strings.Split(filename, "/")
env := fmt.Sprintf("%s/.env", strings.Join(path[:len(path)-2], "/"))
godotenv.Load(env, ".env")

不过这里有一个风险,如果生产环境中刚好有一个和编译环境的源代码路径一模一样并在该路径下存放了另一个版本的 .env 文件时项目的运行 env 就可能会错误。