第一次接触 Golang 的时候,我就对其 Package 感到困惑。在很多语言中,都有类似命名空间(Namespace)的概念,开发者可以嵌套使用命名空间,让项目层次分明。 例如可以分为 Controller 层,Controller 层下面又可以分出 HttpController 和 RpcController,HttpController 下面又可分出 UserController,PostController 等类,类中又可以实现各种方法。

但是在 Golang 中,包似乎是平级的。尽管文件夹形式上可以将包放到父目录和子目录,但是实际上这些包还是平级的。只要你引入了这个包 mypkg,就可以直接使用 mypkg.name 的形式访问这个包下的其它公有符号。例如 net 包和 net/http 包。

为了解决这样的疑惑,探寻出合适的编程惯例,我阅读了很多文章。下面两篇是最推荐的:

  1. 标准包布局:Standard Package Layout (gobeyond.dev)

  2. 将包看作层,而非分组:Packages as layers, not groups (gobeyond.dev)

其思想的核心是:越靠近根部的包,其依赖越少。

假设根部 Package 是 myapp,则属于 myapp 包的类型,都是不涉及其它包的业务类型,例如模型定义,模型的接口定义等。

返回指针还是值类型?

go - Pointers vs. values in parameters and return values - Stack Overflow

通常情况下,除非是你返回的东西需要修改,否则没必要用指针。

“首先,也是最重要的,该方法是否需要修改接收器?如果需要,接收器必须是一个指针。”

“第二是考虑效率。如果接收器很大,例如一个大结构,使用指针接收器会便宜得多。”

“接下来是一致性。如果类型的某些方法必须有指针接收者,其余的也应该如此,所以无论类型如何使用,方法集都是一致的”

Repo 层

Service 层

API 层

获取参数有两种方式。

传统方式:

message := c.PostForm("message")
		nick := c.DefaultPostForm ("nick", "anonymous") // 此方法可以设置默认值

规范方式:

func likes(c *bm.Context) {
	var args struct {
		Sid  int64   `form:"sid" validate:"min=1,required"`
		Lids []int64 `form:"lids,split" validate:"min=1,max=50,dive,min=1"`
	}
	if err := c.Bind(&args); err != nil {
		return
	}
	c.JSON(actSrv.Likes(c, args.Sid, args.Lids))
}

统一错误返回

我们还是学习 B 站,它重写了 JSON 方法:


// JSON serializes the given struct as JSON into the response body.
// It also sets the Content-Type as "application/json".
func (c *Context) JSON(data interface{}, err error) {
	code := http.StatusOK
	c.Error = err
	bcode := ecode.Cause(err)
	// TODO app allow 5xx?
	/*
		if bcode.Code() == -500 {
			code = http.StatusServiceUnavailable
		}
	*/
	writeStatusCode(c.Writer, bcode.Code())
	c.Render(code, render.JSON{
		Code:    bcode.Code(),
		Message: bcode. (),
		Data:    data,
	})
}

atomic.Valuesync/atomic 包中的类型,可以用于原子地读写(Load/Store)任何值,通过指令集的原子操作实现,所以性能比互斥锁(Mutex)好很多。但是不要用来存引用类型,因为不能保证引用类型并发安全。