100 Go Mistakes and How to Avoid Them - 2 Code and project organization

2 Code and project organization

讲代码格式,有点像pythonic。

2.1 Unintended(意外的) variable shadowing(影子)

和作用域有关系。go里面:=和var用来定义新变量,=用来给已有变量赋值。
简单举例:

var n int   
n,err := foo() // 这里因为n和foo在一个作用域 所以这的:=对n来说就是赋值
var n int   
if true {
  n,err := foo() // 这里因为n和foo在不在作用域 所以if里面的n和外面的n不是一个东西
}
var n int
var err error 
if true {
  n,err = foo() // 如果需要是一个东西要这样写
}

说来说去挺麻烦的,其实最简单的办法是借助goland(我是goland党)它会把有问题的地方给你指出来,我平常变量名不够用基本就是依靠这个。
可以看到22行a变成了绿色(图片传上来以后不太明显),36行的a下面有红线

2.2 Unnecessary nested code

代码嵌套层数太多了。 没啥好说的,简单的例子是下面这个。

func TestMis2_Bad(t *testing.T) {
	var a []byte
	a, err := io.ReadAll(nil)
	if err != nil {
		return
	} else {
		b, er := io.ReadAll(nil)
		if er != nil {
			return
		} else {
			fmt.Println(a, b)
		}
	}
}

func TestMis2_Good(t *testing.T) {
	var a []byte
	a, err := io.ReadAll(nil)
	if err != nil { 
		return  // fast fail
	}
	b, err := io.ReadAll(nil)
	if err != nil {
		return  // fast fail
	}
	fmt.Println(a, b)  // happy path
}

2.3 Misusing init function

init可以让package被引入的时候执行,执行顺序是和导入顺序是反的(不需要太关心,因为我们不应该写出依赖init顺序的代码)。
什么时候是滥用的呢?我个人认为比较主观。
书中举了在init中初始化db的例子。

var db *sql.DB
func init() {
  dataSourceName :=
  os.Getenv("MYSQL_DATA_SOURCE_NAME")
  d, err := sql.Open("mysql", dataSourceName)
  if err != nil {
    log.Panic(err)
  }
  err = d.Ping()
  if err != nil {
    log.Panic(err)
  }
  db = d
}

看上去还不错。书中写了三个主要的缺点。

  1. 我个人认为第一点非常重要, 主要观点是把限制了错误处理(They can limit error management),db init出错程序就退出了,但是这个选择应该要交给使用者。
  2. 不便于单元测试。放在init里面,如果db没有准备好,你整个包都没法测试了。
  3. db变成全局变量了,不好。

书中也给出了可能的好的init的用法。用来初始化http路由(但是我个人也没有觉得这样很好)

func init() {
  redirect := func(w http.ResponseWriter, r *http.Request) {
    http.Redirect(w, r, "/", http.StatusFound)
  }
  http.HandleFunc("/blog", redirect)
  http.HandleFunc("/blog/", redirect)
  static := http.FileServer(http.Dir("static"))
  http.Handle("/favicon.ico", static)
  http.Handle("/fonts.css", static)
  http.Handle("/fonts/", static)
  http.Handle("/lib/godoc/", http.StripPrefix("/lib/godoc/", http.HandlerFunc(staticHandler)))
}

所以怎么处理db的初始化更好呢?我的办法是

var db *sql.DB
var initOnce sync.Once

func GetDB() (*sql.DB, error) {
	var err error
	initOnce.Do(func() {
		var _db *sql.DB
		_db, err = sql.Open("", "")
		if err != nil {
			return
		}
		db = _db
	})
	if err != nil {
		return nil, err
	}
	return db, nil
}

每次使用都GetDB就可以了,不需要用到init。error处理也很明确。

我觉得其他地方的滥用(或者说不好用)。恰好也和db有关系,这时候就要提一个我刚开始学go的时候遇到的问题了。使用mysql需要import一下。

import _ "github.com/go-sql-driver/mysql"  // let mysql.init exec  注册mysql.driver

我觉得这点不应该要终端用户来做。不过也没有太好的办法,因为这里注入的sql.driver可能是非常非常多的,甚至可能会是我自己实现的tusql,从这个角度来看,让终端用户来init也是合理(也是没办法的)的。扯远了。

2.4 Overusing getters and setters

主要是说不要把java的get/set习惯带到go来,go希望简洁,所以如果确实没有必要,直接访问field即可。如timer.C。
如果get/set有一些通用的逻辑,如set的时候需要打日志之类的,那是需要setter的。
文章还提到,getter应该Age()而不是GetAge(), setter叫SetAge没问题。这里我表示怀疑,如果字段就是Age怎么办?所以我感觉,叫GetAge也无妨。

2.5 Interface pollution

2.6 Interface on the producer side

2.7 Returning interfaces

这三个谈Interface的使用,Interface的使用是go里非常重要的一点,我自己感觉我用的也不是很地道,有精力(有能力)再讲。

2.8 any says nothing

讲不要滥用any,举了用any的好例子Marshal,sql参数。

In summary, any can be helpful if there is a genuine need for accepting or returning any possible type (for instance, when it comes to marshaling or formatting). In general, we should avoid overgeneralizing the code we write at all costs. Perhaps a little bit of duplicated code might occasionally be better if it improves other aspects such as code expressiveness.

大概是说不要为了稍微少写几行代码使用any而损失代码的表现力

2.9 Being confused about when to use generics

不知道什么时候该用泛型,go1.18加入了泛型,个人觉得还是比较好用的。内容较多,有机会结合实际仔细讲一下。

2.10 Not being aware of the possible problems with type embedding

go提供结构体嵌入,在某些时候非常好用,但是也要知道可能有的问题。内容较多,有机会仔细讲一下。

2.11 Not using the functional options pattern

options pattern好像是我接触go以后才知道的一种东西(可能是我见识短浅了),我觉得这个和go没有默认参数有关系。
它帮助解决参数很多的结构的初始化。有专门讲这个的文章。

2.12 Project misorganization

挖坑。

2.13 Creating utility packages

通常每个人都会有util这种东西,里面放了一些很小的东西。但是不要滥用。
书中的例子是

package util
func NewStringSet(...string) map[string]struct{} {
// ...
}
func SortStringSet(map[string]struct{}) []string {
// ...
}
package stringset
func New(...string) map[string]struct{} { ... }
func Sort(map[string]struct{}) []string { ... }

比如这个stringset更好是作为一个单独的package,而不是在package util里面。
多说依据,stringset单独成package了,New就不要叫NewStringSet了。

2.14 Ignoring package name collisions

变量名不要和package名字重复, 这让我想起了以前写python的时候有个同事写, 然后表示找不到问题

str = "123456"
# n lines ...
i = 10
si = str(i)

2.15 Missing code documentation

相当于注释

2.16 Not using linters(代码静态分析工具)

可以使用代码检查工具,从2.1可以看到goland也带基本的代码检查,我现在是没有专门找代码检查工具来用。

总结

 Avoiding shadowed variables can help prevent mistakes like referencing the wrong variable or confusing readers.
 Avoiding nested levels and keeping the happy path aligned on the left makes building a mental code model easier.
 When initializing variables, remember that init functions have limited error handling and make state handling and testing more complex. In most cases, initializations should be handled as specific functions.
 Forcing the use of getters and setters isn't idiomatic in Go. Being pragmatic and finding the right balance between efficiency and blindly following certain idioms should be the way to go.
 Abstractions should be discovered, not created. To prevent unnecessary complexity, create an interface when you need it and not when you foresee needing it, or if you can at least prove the abstraction to be a valid one.
 Keeping interfaces on the client side avoids unnecessary abstractions.
 To prevent being restricted in terms of flexibility, a function shouldn't return interfaces but concrete implementations in most cases. Conversely, a function should accept interfaces whenever possible.
 Only use any if you need to accept or return any possible type, such as json.Marshal. Otherwise, any doesn't provide meaningful information and can lead to compile-time issues by allowing a caller to call methods with any data type.
 Relying on generics and type parameters can prevent writing boilerplate code to factor out elements or behaviors. However, do not use type parameters prematurely, but only when you see a concrete need for them. Otherwise, they introduce unnecessary abstractions and complexity.
 Using type embedding can also help avoid boilerplate code; however, ensure that doing so doesn't lead to visibility issues where some fields should have remained hidden.
 To handle options conveniently and in an API-friendly manner, use the functional options pattern.
 Following a layout such as project-layout can be a good way to start structuring Go projects, especially if you are looking for existing conventions to standardize a new project.
 Naming is a critical piece of application design. Creating packages such as common, util, and shared doesn't bring much value for the reader. Refactor such packages into meaningful and specific package names.
 To avoid naming collisions between variables and packages, leading to confusion or perhaps even bugs, use unique names for each one. If this isn't feasible, use an import alias to change the qualifier to differentiate the package name from the variable name, or think of a better name.
 To help clients and maintainers understand your code's purpose, document exported elements.
 To improve code quality and consistency, use linters and formatters.
机翻
 避免阴影变量有助于防止错误,例如引用错误的变量或使读者感到困惑。
避免嵌套关卡并保持快乐路径在左侧对齐,使构建心理代码模型更容易。
 初始化变量时,请记住 init 函数的错误处理有限,使状态处理和测试更加复杂。在大多数情况下,初始化应作为特定函数处理。
 强制使用 getter 和 setter 在 Go 中不是惯用语。务实并在效率和盲目遵循某些习语之间找到适当的平衡应该是要走的路。
 抽象应该被发现,而不是创建。为了防止不必要的复杂性,请在需要它时创建一个接口,而不是在你预见到需要它时创建一个接口,或者如果你至少可以证明抽象是一个有效的抽象。
 将接口保留在客户端可避免不必要的抽象。
 为了防止在灵活性方面受到限制,函数在大多数情况下不应返回接口,而应返回具体的实现。相反,函数应尽可能接受接口。
 仅在需要接受或返回任何可能的类型(例如 json)时使用 any 。元帅。否则,any 不会提供有意义的信息,并且允许调用方调用具有任何数据类型的方法,从而导致编译时问题。
 依赖泛型和类型参数可以防止编写样板代码来分解元素或行为。但是,不要过早地使用类型参数,而要仅在您看到对它们的具体需求时才使用类型参数。否则,它们会引入不必要的抽象和复杂性。
 使用类型嵌入也有助于避免样板代码;但是,请确保这样做不会导致某些字段应保持隐藏状态的可见性问题。
 要以 API 友好的方式方便地处理选项,请使用功能选项模式。
 遵循诸如项目布局之类的布局可能是开始构建 Go 项目的好方法,特别是如果您正在寻找现有约定来标准化新项目。
 命名是应用程序设计的关键部分。创建诸如 common、util 和 shared 之类的包不会给读者带来太多价值。将此类包重构为有意义且特定的包名称。
 为避免变量和包之间的命名冲突,导致混淆甚至错误,请为每个变量和包使用唯一的名称。如果这不可行,请使用导入别名更改限定符以区分包名称与变量名称,或者考虑一个更好的名称。
 为了帮助客户和维护人员了解代码的目的,请记录导出的元素。
 要提高代码质量和一致性,请使用 linters 和格式化程序。

posted @   xiaotushaoxia  阅读(67)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· winform 绘制太阳,地球,月球 运作规律
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
点击右上角即可分享
微信分享提示