go开发规范指引
工程目录说明
project-layout 是一个 Go 社区维护的 Go 项目目录结构标准,它的目标是提供一种一致性的、易于理解和使用的目录结构,从而帮助开发者更好地组织和管理自己的代码
https://github.com/golang-standards/project-layout/blob/master/README_zh.md
pkg: 公共可复用的库代码,可以被其他项目导入和使用,放置可复用的文件、库,例如 commons、utils、logger的封装
/internal
● 私有代码,不希望被依赖者引入。与之对应的是pkg目录。
● 项目内应用之间公共部分放在/internal/pkg
internal: 包含应用程序的私有代码,组织应用的各个模块。
○ Controller 负载处理用户的请求,面向用户暴露的接口逻辑写在这里
○ config: 存放配置相关文件。
○ http: 包含HTTP服务相关的文件,比如处理器和中间件。
○ repository: 数据访问层,负责与数据库交互。
○ service: 业务逻辑层,服务类放这里。
常见库
一个精心整理的go语言框架、库、软件的集合
https://github.com/avelino/awesome-go?tab=readme-ov-file#contents
库集合
https://blog.csdn.net/weixin_45541665/article/details/134945385?utm_medium=distribute.pc_relevant.none-task-blog-2defaultbaidujs_baidulandingword~default-0-134945385-blog-136830124.235v43pc_blog_bottom_relevance_base7&spm=1001.2101.3001.4242.1&utm_relevant_index=3
其他:https://github.com/xingshaocheng/architect-awesome
https://github.com/vinta/awesome-python
1.web框架
Fuego
2.配置config
https://github.com/gookit/config?tab=readme-ov-file
3.代码生成
https://github.com/mafulong/godal/blob/main/README_CN.md
基于 Mysql 的建表语句快速生成对应 Golang 的 Model,可直接被 ORM 框架 GORM 使用
编码规范指南
https://github.com/xxjwxc/uber_go_guide_cn
1.Testing
Go语言内置了丰富的测试框架,通过编写测试用例来验证代码功能。
import “testing”
func TestAdd(t *testing.T) {
result := add(2, 3)
expected := 5
if result != expected {
t.Errorf(“Expected %d, but got %d”, expected, result)
}
}
2.一致性
1).导入应该分为两组:标准库、其他库
import (
“fmt”
“os”
“go.uber.org/atomic”
“golang.org/x/sync/errgroup”
)
2)相似的声明放在一组
type Operation int
const (
Add Operation = iota + 1
Subtract
Multiply
)
const EnvVar = “MY_ENV”
3).本地变量声明
如果将变量明确设置为某个值,则应使用短变量声明形式 (:=)。:= 允许编译器自动推断变量的类型,会在首次声明变量时同时进行初始化。
而在包级别(全局变量)是不允许使用 := 的(var可以用于声明包级别的全局变量。这些变量在整个包中都能够访问)。
当想要在函数的多个返回值中选择性地使用一个或者多个时,var声明可以提供易于阅读的方式。
var err error
_, err = someFunction()
if err != nil {
// handle error
}
4)包名
当命名包时,请按下面规则选择一个名称:
全部小写。没有大写或下划线。
大多数使用命名导入的情况下,不需要重命名。
简短而简洁。请记住,在每个使用的地方都完整标识了该名称。
不用复数。例如net/url,而不是net/urls。
不要用“common”,“util”,“shared”或“lib”。这些是不好的,信息量不足的名称。
5)导入别名
如果程序包名称与导入路径的最后一个元素不匹配,则必须使用导入别名。
import (
“net/http”
client “example.com/client-go”
trace “example.com/trace/v2”
)
6).结构体中的嵌入
嵌入式类型(例如 mutex)应位于结构体内的字段列表的顶部,并且必须有一个空行将嵌入式字段与常规字段分隔开。
type Client struct {
version int
http.Client
} type Client struct {
http.Client
version int
}
3.nil操作( fmt.Println(nil == nil) 返回true or false ?)
1).nil的数据类型
零值(zero value):Go语言中每个类型都有一个零值,这是该类型的默认值,根据类型的不同而不同。例如,对于基本数据类型,其零值是0(数字类型)、‘’(字符串)、false(布尔类型)。对于数组和结构体,其零值是每个元素或字段的零值。对于接口,其零值是nil。
空值(nil):在Go语言中,nil是一个预定义的标识符,用于表示指针、通道(channel)、映射(map)、切片(slice)、函数以及接口类型的“零值”。它相当于这些类型的“无”或“不存在”。例如,一个nil指针不指向任何内存地址,而一个nil通道不连接任何发送者或接收者。
● 单独的一个nil值本身没有类型,只有通过上下文,判断其赋值对象,才能判断其类型
● 不同类型的nil值无法进行比较,不同类型的nil值,内存大小也不一样
● nil其实包含两个值,分别是type和data,nil会被间接转换成其动态类型和值。即便两个接口都被设为 nil,这两个接口的类型信息却可能不相同,从而导致比较出错。
func main() {
var p *struct{} = nil
fmt.Println(unsafe.Sizeof§) // 8
var s []int = nil
fmt.Println(unsafe.Sizeof(s)) // 24
var m map[int]bool = nil
fmt.Println(unsafe.Sizeof(m)) // 8
var c chan string = nil
fmt.Println(unsafe.Sizeof©) // 8
var f func() = nil
fmt.Println(unsafe.Sizeof(f)) // 8
var i interface{} = nil
fmt.Println(unsafe.Sizeof(i)) // 16
fmt.Println(p == s) //报错,mismatched types *struct{} and []int
}
2). 在 Golang 中,你不能将 nil 直接赋值给非接口类型。
3). 切片判空慎用nil
func F() {
// 定义变量
var s []string
fmt.Printf(“1:nil=%t\n”, s == nil) // true
// 组合字面量方式
s = []string{}
fmt.Printf(“1:nil=%t\n”, s == nil) // false
// make方式
s = make([]string, 0)
fmt.Printf(“1:nil=%t\n”, s == nil) // false
}
func A()[]string{
…
return nil
}
func B(){
c := A()
// 判断c是否是空数组
if c == nil {} // 不推荐
if len© == 0 {} // 推荐
}
4).any、interface{}和nil判断
func F(){
var x *string
var y *string
BothNil := func(a any, b any) bool {
return a == nil && b == nil
}
BothNilInterface := func(a interface{}, b interface{}) bool {
return a == nil && b == nil
}
fmt.Println(x == nil && y == nil)
fmt.Println(BothNil(nil, nil))
fmt.Println(BothNil(x, y))
fmt.Println(BothNilInterface(nil, nil))
fmt.Println(BothNilInterface(x, y))
}
因为any/interface{}数据,其定义中不仅包含其所代表的值,同样还有其代表值的类型。
直接使用any/interface{}做nil判断,不仅需要判断data是否为nil,
还需要判断其类型是否为空(类型只要被赋值interface{},一般就不会为空)
4.错误包装:使用github.com/pkg/errors来包装错误信息
fmt.Errorf 相对简单直接,适用于不需要详细堆栈跟踪的错误处理场景。而如果需要更详细的错误上下文和堆栈追踪,pkg/errors 是一个更好的选择。
package main
import (
“fmt”
“github.com/pkg/errors”
“os”
)
func main() {
err := readFile(“nonexistent.txt”)
if err != nil {
// 打印错误信息时会显示完整的堆栈信息
fmt.Printf(“An error occurred: %+v\n”, err) //%+v 使得调用栈信息得到显示
}
}
func readFile(filename string) error {
f, err := os.Open(filename)
if err != nil {
// 包装原始错误并添加注解信息
return errors.Wrap(err, “failed to open file”)
}
defer f.Close()
// 执行其他文件操作
return nil
}
5.指针
对于少量数据,无需传递指针;对于大量数据的 struct 可以考虑使用指针;传入的参数是 map,slice,chan时不传递指针,因为 map,slice,chan 是引用类型,不需要传递指针的指针。
package main
import “fmt”
type LargeStruct struct {
Data [1000]int
}
func updateStruct(ls *LargeStruct) {
ls.Data[0] = 1
}
func main() {
ls := LargeStruct{}
updateStruct(&ls)
fmt.Println(“First Element:”, ls.Data[0]) // 输出: First Element: 1
}
#在这里,LargeStruct 结构体比较大,使用指针可以避免拷贝整个结构体。
func modifyMap(m map[string]int) {
m[“key”] = 42
}
func modifySlice(s []int) {
s[0] = 42
}
func main() {
// map 示例
myMap := make(map[string]int)
modifyMap(myMap)
fmt.Println(“Map value:”, myMap[“key”]) // 输出: Map value: 42
// slice 示例
mySlice := []int{1, 2, 3}
modifySlice(mySlice)
fmt.Println("Slice value:", mySlice[0]) // 输出: Slice value: 42
}
#map 和 slice 都是通过引用类型传递的。在函数中进行的修改会反映到原来的变量上。
6.值比较处理
true/false求值 ,当明确变量expr为bool时,禁止使用==或!=与true/false比较,应该使用expr或!expr;判断某个整数表达式expr是否为零时,禁止使用!expr,应该使用expr == 0
7.不要使用 panic
在Go中,当某个函数调用发生无法处理的错误时,它可以选择引发panic。panic是一种表示致命错误的异常情况,它会导致程序停止执行并打印出panic信息。
在生产环境中运行的代码必须避免出现 panic。panic 是 级联失败 的主要根源 。如果发生错误,该函数必须返回错误,并允许调用方决定如何处理它。
package main
import (
“fmt”
“io/ioutil”
“os”
)
func readFile(filename string) ([]byte, error) {
data, err := ioutil.ReadFile(filename)
if err != nil {
// 不使用 panic,而是返回错误
return nil, fmt.Errorf(“failed to read file: %w”, err)
}
return data, nil
}
func main() {
data, err := readFile(“config.txt”)
if err != nil {
fmt.Println(“Error:”, err)
// 根据需要处理错误,比如重试、使用默认值等
return
}
fmt.Println(“File content:”, string(data))
}
package main
import (
“fmt”
“io/ioutil”
“net/http”
)
func fetchData(url string) []byte {
resp, err := http.Get(url)
if err != nil {
// 使用 panic,这不是生产代码中理想的做法
panic(fmt.Sprintf(“failed to fetch data: %v”, err))
}
defer resp.Body.Close()
data, err := ioutil.ReadAll(resp.Body)
if err != nil {panic(fmt.Sprintf("failed to read response body: %v", err))
}
return data
}
func main() {
data := fetchData(“http://example1.com/api/data”)
fmt.Println(“Data:”, string(data))
}
8.结构体处理
1). 进行指针操作时必须判断该指针是否为nil,防止程序panic,尤其在进行结构体Unmarshal时
package main
import (
“encoding/json”
“fmt”
)
type Address struct {
City string json:"city"
State string json:"state"
}
type Person struct {
Name string json:"name"
Age int json:"age"
Address *Address json:"address"
}
func main() {
// JSON数据包含address字段,但值为null
data := []byte({"name": "Alice", "age": 30, "address": null})
var person Person
err := json.Unmarshal(data, &person)
if err != nil {fmt.Println("Error unmarshalling JSON:", err)return
}// 在访问指针类型成分之前需要进行nil检查
if person.Address != nil {fmt.Println("City:", person.Address.City)fmt.Println("State:", person.Address.State)
} else {fmt.Println("Address is nil")
}// 强制访问指针类型成分则会引发panic
// fmt.Println("City:", person.Address.City) // 这里如果不进行检测就直接访问,会导致panic
}
2). 使用字段名初始化结构:初始化结构时,几乎应该始终指定字段名。
k := User{“John”, “Doe”, true}
k := User{
FirstName: “John”,
LastName: “Doe”,
Admin: true,
}
3). 在序列化结构中使用字段标记
任何序列化到JSON、YAML、, 或其他支持基于标记的字段命名的格式应使用相关标记进行注释。
type Stock struct {
Price int
Name string
}
bytes, err := json.Marshal(Stock{
Price: 137,
Name: “UBER”,
}) type Stock struct {
Price int json:"price"
Name string json:"name"
// Safe to rename Name to Symbol.
}
bytes, err := json.Marshal(Stock{
Price: 137,
Name: “UBER”,
})
4). json序列化忽略某个字段
Go语言的结构体提供标签功能,在结构体标签中使用 - 操作符就可以对不需要序列化的字段做特殊处理。
5). json序列化忽略空值字段
我们使用json.Marshal进行序列化时不会忽略struct中的空值,默认输出字段的类型零值(string类型零值是"",对象类型的零值是nil…),如果我们想在序列化时忽略掉这些没有值的字段时,可以在结构体标签中中添加omitempty tag
6). 结构体的私有字段无法被序列化
package main
import (
“encoding/json”
“fmt”
)
type User struct {
Name string json:"name"
Email string json:"email,omitempty"
Age int json: "age"
Hobby string json:"-"
phone string
}
func main() {
u1 := User{
Name: “aaron”,
Hobby: “football”,
phone: “1111111111”,
}
b, err := json.Marshal(u1)
if err != nil {
fmt.Printf(“json.Marshal failed, err:%v\n”, err)
return
}
fmt.Printf(“str:%s\n”, b)
}
7)在 Go 语言中,request请求参数中的int 类型的变量默认值为 0。如果要判断一个 int 类型的字段 IsAdminPublished 在请求参数中是否有传递值,通过使用一个指针来区分未传递和传递 0 的情况
//request定义
type AiModelRepoRequest struct {
IsAdminPublished *int json:"isAdminPublished,omitempty"
}
//赋值
var IsAdminPublished = 1
repoReq.IsAdminPublished = &IsAdminPublished
//判断
if requestParams.IsAdminPublished != nil {
entity.IsAdminPublished = requestParams.IsAdminPublished //根据指针取出值requestParams.IsAdminPublished
}
9.不要一劳永逸地使用 goroutine,必须确保每个协程都能退出
启动一个协程就会做一个入栈操作,在系统不退出的情况下,协程也没有设置退出条件,则相当于协程失去了控制,它占用的资源无法回收,可能会导致内存泄露。
● 必须有一个可预测的停止运行时间;
● 必须有一种方法可以向 goroutine 发出信号它应该停止
package main
import (
“fmt”
“time”
)
func worker(done chan struct{}) {
for {
select {
case msg:=<-done:
fmt.Println(“Exiting goroutine.{}”,msg)
return
default:
fmt.Println(“Still working.”)
time.Sleep(1 * time.Second)
}
}
}
func main() {
done := make(chan struct{})
go worker(done)time.Sleep(3 * time.Second)
fmt.Println("Closing.")
close(done)//done <- struct{}{}
time.Sleep(1 * time.Second)
fmt.Println("Exited main().")
}
1.使用chan struct{},因为只是用来信号通知,并不需要传递具体数据,这也是 Go 语言中的一种习惯用法,用 struct{} 节省内存。
2.关闭channel通常用于通知goroutine退出。关闭一个channel后,从该channel接收数据的操作会立即返回该值类型的零值(比如,如果是bool类型,返回false;如果是int类型,返回0)。同时尝试向一个已经关闭的channel发送数据会引发panic(send on closed channel)。
10.确保并发安全
敏感操作如果未作并发安全限制,可导致数据读写异常,造成业务逻辑限制被绕过。可通过同步锁或者原子操作进行防护。
使用互斥锁 (Mutex)
互斥锁是最常见的并发控制手段之一,可以用来保证同一时刻只有一个协程能访问某段代码。
package main
import (
“fmt”
“sync”
)
type SafeCounter struct {
mu sync.Mutex
value int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
func (c *SafeCounter) GetValue() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}
func main() {
counter := SafeCounter{}
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {wg.Add(1)go func() {counter.Increment()wg.Done()}()
}
wg.Wait()
fmt.Println("Final Counter Value:", counter.GetValue())
}
使用原子操作
Go提供了一些原子操作函数,可以用于基本的数据类型,如int32, int64等,来实现并发安全的增减。
package main
import (
“fmt”
“sync”
“sync/atomic”
)
type AtomicCounter struct {
value int64
}
func (c *AtomicCounter) Increment() {
atomic.AddInt64(&c.value, 1)
}
func (c *AtomicCounter) GetValue() int64 {
return atomic.LoadInt64(&c.value)
}
func main() {
counter := AtomicCounter{}
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {wg.Add(1)go func() {counter.Increment()wg.Done()}()
}
wg.Wait()
fmt.Println("Final Counter Value:", counter.GetValue())
}
使用原子操作
Go提供了一些原子操作函数,可以用于基本的数据类型,如int32, int64等,来实现并发安全的增减。
package main
import (
“fmt”
)
func main() {
ch := make(chan int)
done := make(chan bool)
// Consumer
go func() {value := 0for v := range ch {value += v}fmt.Println("Final Counter Value:", value)done <- true
}()// Producer
for i := 0; i < 1000; i++ {ch <- 1
}close(ch)
<-done
}
以上注意事项
避免死锁:尽量减少锁的粒度,不要在多个锁之间互相等待。
选择合适的工具:对于简单计数等操作,使用sync/atomic,对于复杂数据结构,使用sync.Mutex或sync.RWMutex。
确保使用defer释放锁:确保每个Lock都有对应的Unlock,可以使用defer来避免遗忘。
优先使用通道解决并发问题:在适合的场景下,通道是Go语言中推荐的并发模式,有助于代码的可读性和可靠性。
11.SQL安全
所有的查询语句建议使用数据库提供的参数化查询接口,参数化的语句使用参数而不是将用户输入变量嵌入到 SQL 语句中,即不要直接拼接 SQL 语句。
userInput := “tom;drop table users;”
// 安全的,会被转义
db.Where(“name = ?”, userInput).First(&user)
// SQL 注入
db.Where(fmt.Sprintf(“name = %v”, userInput)).First(&user)
userInput := “tom;drop table users;”
// 会被转义
db.First(&user, “name = ?”, userInput)
// SQL 注入
db.First(&user, fmt.Sprintf(“name = %v”, userInput))
------当通过用户输入的整形主键检索记录时,你应该对变量进行类型检查------
userInputID := “1=1;drop table users;”
// 安全的,返回 err
id,err := strconv.Atoi(userInputID)
if err != nil {
return error
}
db.First(&user, id)
// 不安全的,产生SQL 注入
db.First(&user, userInputID)
// SELECT * FROM users WHERE 1=1;drop table users;
