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

相关推荐
2301_795167202 小时前
玩转Rust高级应用 如何进行理解Refutability(可反驳性): 模式是否会匹配失效
开发语言·算法·rust
lllsure2 小时前
【Python】Dict(字典)
开发语言·python
云知谷2 小时前
【C/C++基本功】C/C++江湖风云录:void* 的江湖传说
c语言·开发语言·c++·软件工程·团队开发
脚踏实地的大梦想家3 小时前
【Go】P19 Go语言并发编程核心(三):从 Channel 安全到互斥锁
开发语言·安全·golang
逻极3 小时前
Rust数据类型(下):复合类型详解
开发语言·后端·rust
星释3 小时前
Rust 练习册 12:所有权系统
开发语言·后端·rust
tianyuanwo3 小时前
Rust开发完全指南:从入门到与Python高效融合
开发语言·python·rust
民乐团扒谱机4 小时前
脉冲在克尔效应下的频谱展宽仿真:原理与 MATLAB 实现
开发语言·matlab·光电·非线性光学·克尔效应
yuan199974 小时前
基于扩展卡尔曼滤波的电池荷电状态估算的MATLAB实现
开发语言·matlab