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-2~default~baidujs_baidulandingword\~default-0-134945385-blog-136830124.235^v43^pc_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 mapintbool = 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 1000int

}

func updateStruct(ls *LargeStruct) {

ls.Data0 = 1

}

func main() {

ls := LargeStruct{}
updateStruct(&ls)
fmt.Println("First Element:", ls.Data0) // 输出: First Element: 1
}
#在这里,LargeStruct 结构体比较大,使用指针可以避免拷贝整个结构体。

func modifyMap(m mapstringint) {

m"key" = 42

}

func modifySlice(s \[\]int) {

s0 = 42

}

func main() {

// map 示例

myMap := make(mapstringint)

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 := 0
    for 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;

相关推荐
懂点Tech1 小时前
ctfshow MISC入门图片篇(信息附加):misc41解题思路
开发语言·python
catchadmin2 小时前
PHP 8.7 协程新提案 让跨协程代码共享请求身份
开发语言·php
念何架构之路3 小时前
go-grpc设计思想
开发语言·后端·golang
酷炫码神3 小时前
Day 10|Pinia 状态管理:把散落在各组件的数据,收进一个全局仓库
android·开发语言·javascript
microrain3 小时前
SagooIoT 的缓存换后端只改一行配置,文件后端连 Redis 都不需要
物联网·缓存·golang·sagooiot
Elastic 中国社区官方博客4 小时前
我们如何通过 70 多个拉取请求,将 OpenTelemetry Java agent 迁移到 invokedynamic
java·大数据·开发语言·elasticsearch·搜索引擎·全文检索·es
tyn1884 小时前
记录本地安装nacos服务的方法及异常
java·开发语言
步行cgn5 小时前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
喵了几个咪5 小时前
GoWind ShortPlay 深度拆解:三服务 BFF + Core,一条请求的完整工程链路
golang·gowind
传奇开心果编程6 小时前
【Rust入门练中学】第5课:引用与借用
开发语言·rust·引用·编程语言理论·通用编程语言·借用