最佳实际是一些不成文的经验总结,遵循最佳实际能够使咱们站在前人的肩膀上,防止某些常见谬误,写出更好的代码。原文: Golang Best Practices (Top 20)
只须要花上 10 分钟浏览本文,就能够帮忙你更高效编写 Go 代码。
20: 应用适当缩进
良好的缩进使代码更具可读性,始终应用制表符或空格(最好是制表符),并遵循 Go 规范的缩进约定。
package main
import "fmt"
func main() {
for i := 0; i < 5; i++ {fmt.Println("Hello, World!")
}
}
运行 gofmt
依据 Go 规范主动格式化 (缩进) 代码。
$ gofmt -w your_file.go
19: 正确导入软件包
只导入须要的包,并格式化导入局部,将规范库包、第三方包和本人的包分组。
package main
import (
"fmt"
"math/rand"
"time"
)
18: 应用描述性变量名和函数名
- 有意义的名称: 应用可能传播变量用处的名称。
- 驼峰表示法(CamelCase): 以小写字母结尾,前面每个单词的首字母大写。
- * 简短的名称: 简短、简洁的名称对于作用域小、寿命短的变量是能够承受的。
- 不应用缩写: 防止应用费解的缩写和首字母缩略词,尽量应用描述性名称。
- 一致性: 在整个代码库中放弃命名一致性。
package main
import "fmt"
func main() {
// 应用有意义的名称申明变量
userName := "John Doe" // CamelCase: 以小写字母结尾,前面的单词大写。itemCount := 10 // 简短的名称: 对于小作用域变量来说,短而简洁。isReady := true // 不应用缩写: 防止应用费解的缩写或首字母缩写。// 显示变量值
fmt.Println("User Name:", userName)
fmt.Println("Item Count:", itemCount)
fmt.Println("Is Ready:", isReady)
}
// 对包级变量应用 mixedCase
var exportedVariable int = 42
// 函数名应该是描述性的
func calculateSumOfNumbers(a, b int) int {return a + b}
// 一致性: 在整个代码库中放弃命名的一致性。
17: 限度每行长度
尽可能将每行代码字符数管制在 80 个以下,以进步可读性。
package main
import (
"fmt"
"math"
)
func main() {result := calculateHypotenuse(3, 4)
fmt.Println("Hypotenuse:", result)
}
func calculateHypotenuse(a, b float64) float64 {return math.Sqrt(a*a + b*b)
}
16: 将魔法值定义为常量
防止在代码中应用魔法值。魔法值是硬编码的数字或字符串,扩散在代码中,不足上下文,很难了解其目标。将魔法值定义为常量,能够使代码更易于保护。
package main
import "fmt"
const (
// 为重试的最大次数定义常量
MaxRetries = 3
// 为默认超时 (以秒为单位) 定义常量
DefaultTimeout = 30
)
func main() {
retries := 0
timeout := DefaultTimeout
for retries < MaxRetries {fmt.Printf("Attempting operation (Retry %d) with timeout: %d seconds\n", retries+1, timeout)
// ... 代码逻辑 ...
retries++
}
}
15. 错误处理
Go 激励开发者显式处理错误,起因如下:
- 安全性: 错误处理确保意外问题不会导致程序 panic 或忽然解体。
- 清晰性: 显式错误处理使代码更具可读性,并有助于辨认可能产生谬误的中央。
- 可调试性: 处理错误为调试和故障排除提供了有价值的信息。
咱们创立一个简略程序来读取文件并正确处理错误:
package main
import (
"fmt"
"os"
)
func main() {
// Open a file
file, err := os.Open("example.txt")
if err != nil {
// 处理错误
fmt.Println("Error opening the file:", err)
return
}
defer file.Close() // 完结时敞开文件
// 读取文件内容
buffer := make([]byte, 1024)
_, err = file.Read(buffer)
if err != nil {
// 处理错误
fmt.Println("Error reading the file:", err)
return
}
// 打印文件内容
fmt.Println("File content:", string(buffer))
}
14. 防止应用全局变量
尽量减少应用全局变量,全局变量可能导致不可预测的行为,使调试变得艰难,并妨碍代码重用,还会在程序的不同局部之间引入不必要的依赖关系。相同,通过函数参数传递数据并返回值。
咱们编写一个简略的 Go 程序来阐明防止全局变量的概念:
package main
import ("fmt")
func main() {
// 在 main 函数中申明并初始化变量
message := "Hello, Go!"
// 调用应用局部变量的函数
printMessage(message)
}
// printMessage 是带参数的函数
func printMessage(msg string) {fmt.Println(msg)
}
13: 应用构造体解决简单数据
通过构造体将相干的数据字段和办法组合在一起,使代码更有组织性和可读性。
上面是一个残缺示例程序,演示了构造体在 Go 中的利用:
package main
import ("fmt")
// 定义名为 Person 的构造体来示意人的信息。type Person struct {
FirstName string // 名字
LastName string // 姓氏
Age int // 年龄
}
func main() {
// 创立 Person 构造的实例并初始化字段。person := Person{
FirstName: "John",
LastName: "Doe",
Age: 30,
}
// 拜访并打印构造体字段的值。fmt.Println("First Name:", person.FirstName) // 打印名字
fmt.Println("Last Name:", person.LastName) // 打印姓氏
fmt.Println("Age:", person.Age) // 打印年龄
}
12. 对代码进行正文
增加正文来解释代码的性能,特地是对于简单或不显著的局部。
单行正文
单行正文以 //
结尾,用来解释特定的代码行。
package main
import "fmt"
func main() {
// 单行正文
fmt.Println("Hello, World!") // 打印问候语
}
多行正文
多行正文蕴含在 /* */
中,用于较长的解释或逾越多行的正文。
package main
import "fmt"
func main() {
/*
多行正文。能够逾越多行。*/
fmt.Println("Hello, World!") // 打印问候语
}
函数正文
在函数中增加正文,解释函数的用处、参数和返回值。函数正文应用 ’godoc‘ 款式。
package main
import "fmt"
// greetUser 通过名称向用户示意欢送。// Parameters:
// name (string): 欢送的用户名
// Returns:
// string: 问候语
func greetUser(name string) string {return "Hello," + name + "!"}
func main() {
userName := "Alice"
greeting := greetUser(userName)
fmt.Println(greeting)
}
包正文
在 Go 文件顶部增加正文来形容包的用处,应用雷同的 ’godoc‘ 款式。
package main
import "fmt"
// 这是 Go 程序的主包。// 蕴含入口 (main) 函数。func main() {fmt.Println("Hello, World!")
}
11: 应用 goroutine 解决并发
利用 goroutine 来高效执行并发操作。在 Go 语言中,gooutine 是轻量级的并发执行线程,可能并发的运行函数,而没有传统线程的开销。从而帮忙咱们编写高度并发和高效的程序。
咱们用一个简略的例子来阐明:
package main
import (
"fmt"
"time"
)
// 并发运行的函数
func printNumbers() {
for i := 1; i <= 5; i++ {fmt.Printf("%d", i)
time.Sleep(100 * time.Millisecond)
}
}
// 在主 goroutine 中运行的函数
func main() {
// 开始 goroutine
go printNumbers()
// 继续执行 main
for i := 0; i < 2; i++ {fmt.Println("Hello")
time.Sleep(200 * time.Millisecond)
}
// 确保在 goroutine 在退出前实现
time.Sleep(1 * time.Second)
}
10: 用 Recover 解决 panic
应用 recover
来优雅解决 panic 和避免程序解体。在 Go 中,panic 是可能导致程序解体的意外运行时谬误。然而,Go 提供了一种名为 recover
的机制来优雅的解决 panic。
咱们用一个简略的例子来阐明:
package main
import "fmt"
// 可能会 panic 的函数
func riskyOperation() {defer func() {if r := recover(); r != nil {
// 从 panic 中 Recover,并优雅解决
fmt.Println("Recovered from panic:", r)
}
}()
// 模仿 panic 条件
panic("Oops! Something went wrong.")
}
func main() {fmt.Println("Start of the program.")
// 在从 panic 中复原的函数中调用有危险的操作
riskyOperation()
fmt.Println("End of the program.")
}
9. 防止应用 ’init’ 函数
除非必要,否则防止应用 init
函数,因为这会使代码更难了解和保护。
更好的办法是将初始化逻辑移到显式调用的惯例函数中,通常从 main
中调用,从而提供了更好的管制,加强了代码可读性,并简化了测试。
上面是一个简略的 Go 程序,演示了如何防止应用 init
函数:
package main
import ("fmt")
// InitializeConfig 初始化配置
func InitializeConfig() {
// 初始化配置参数
fmt.Println("Initializing configuration...")
}
// InitializeDatabase 初始化数据库连贯
func InitializeDatabase() {
// 初始化数据库连贯
fmt.Println("Initializing database...")
}
func main() {
// 显示调用初始化函数
InitializeConfig()
InitializeDatabase()
// 主代码逻辑
fmt.Println("Main program logic...")
}
8: 应用 Defer 进行资源清理
defer
能够将函数的执行提早到函数返回的时候,通常用于敞开文件、解锁互斥锁或开释其余资源等工作。
这确保了即便在存在谬误的状况下也能执行清理操作。
咱们创立一个简略的程序,从文件中读取数据,应用 defer
确保文件被正确敞开,不必管可能产生的任何谬误:
package main
import (
"fmt"
"os"
)
func main() {// 关上文件(用理论文件名替换 "example.txt")
file, err := os.Open("example.txt")
if err != nil {fmt.Println("Error opening the file:", err)
return // Exit the program on error
}
defer file.Close() // 确保函数退出时敞开文件
// 读取并打印文件内容
data := make([]byte, 100)
n, err := file.Read(data)
if err != nil {fmt.Println("Error reading the file:", err)
return // 出错退出程序
}
fmt.Printf("Read %d bytes: %s\n", n, data[:n])
}
7: 抉择复合字面值而不是构造函数
应用复合字面值来创立 struct 实例,而不是构造函数。
为什么要应用复合文字?
复合字面值提供了几个长处:
- 简洁
- 易读
- 灵便
咱们用一个简略例子来阐明:
package main
import ("fmt")
// 定义一个示意人的构造类型
type Person struct {
FirstName string // 名字
LastName string // 姓氏
Age int // 年龄
}
func main() {
// 应用复合字面量创立 Person 实例
person := Person{
FirstName: "John", // 初始化 FirstName 字段
LastName: "Doe", // 初始化 LastName 字段
Age: 30, // 初始化 Age 字段
}
// Printing the person's information
fmt.Println("Person Details:")
fmt.Println("First Name:", person.FirstName) // 拜访并打印 FirstName 字段
fmt.Println("Last Name:", person.LastName) // 拜访并打印 LastName 字段
fmt.Println("Age:", person.Age) // 拜访并打印 Age 字段
}
6. 最小化性能参数
在 Go 中,编写洁净高效的代码至关重要。实现这一点的一种办法是尽量减少函数参数的数量,从而进步代码的可维护性和可读性。
咱们用一个简略例子来阐明:
package main
import "fmt"
// Option 构造保留配置参数
type Option struct {
Port int
Timeout int
}
// ServerConfig 是承受 Option 构造作为参数的函数
func ServerConfig(opt Option) {fmt.Printf("Server configuration - Port: %d, Timeout: %d seconds\n", opt.Port, opt.Timeout)
}
func main() {
// 创立 Option 构造并初始化为默认值
defaultConfig := Option{
Port: 8080,
Timeout: 30,
}
// 用默认值配置服务
ServerConfig(defaultConfig)
// 创立新的 Option 构造并批改端口
customConfig := Option{Port: 9090,}
// 用自定义端口和默认 Timeout 配置服务
ServerConfig(customConfig)
}
在这个例子中,咱们定义了一个 Option
构造体来保留服务器配置参数。咱们没有向 ServerConfig
函数传递多个参数,而是应用 Option
构造体,这使得代码更易于保护和扩大。这种办法在具备大量配置参数的函数时特地有用。
5: 清晰起见,应用显式返回值而不是命名返回值
命名返回值在 Go 中很罕用,但有时会使代码不那么清晰,特地是在较大的代码库中。
咱们用一个简略例子来看看它们的区别。
package main
import "fmt"
// namedReturn 演示命名返回值。func namedReturn(x, y int) (result int) {
result = x + y
return
}
// explicitReturn 演示显式返回值。func explicitReturn(x, y int) int {return x + y}
func main() {
// 命名返回值
sum1 := namedReturn(3, 5)
fmt.Println("Named Return:", sum1)
// 显示返回值
sum2 := explicitReturn(3, 5)
fmt.Println("Explicit Return:", sum2)
}
下面的示例程序中有两个函数,namedReturn
和explicitReturn
,以下是它们的不同之处:
namedReturn
应用命名返回值result
。尽管函数返回的内容很分明,但在更简单的函数中可能不会很显著。explicitReturn
间接返回后果,这样更简略、明确。
4: 将函数复杂性放弃在最低限度
函数复杂性是指函数代码的复杂程度、嵌套水平和分支水平。放弃较低的函数复杂度会使代码更具可读性、可维护性,并且不易出错。
咱们用一个简略的例子来摸索这个概念:
package main
import ("fmt")
// CalculateSum 返回两个数字的和
func CalculateSum(a, b int) int {return a + b}
// PrintSum 打印两个数字的和
func PrintSum() {
x := 5
y := 3
sum := CalculateSum(x, y)
fmt.Printf("Sum of %d and %d is %d\n", x, y, sum)
}
func main() {
// 调用 PrintSum 函数来演示最小函数复杂度
PrintSum()}
在下面的示例程序中:
- 咱们定义了两个函数,
CalculateSum
和PrintSum
,各自具备特定职责。 CalculateSum
是一个简略函数,用于计算两个数字的和。PrintSum
调用CalculateSum
计算并打印 5 和 3 的和。- 通过放弃函数简洁并专一于单个工作,咱们放弃了较低的函数复杂性,进步了代码的可读性和可维护性。
3: 防止暗藏变量
当在较窄的范畴内申明具备雷同名称的新变量时,就会产生变量暗藏,这可能导致意外行为。这种状况下,具备雷同名称的内部变量会被暗藏,使其在该作用域中不可拜访。尽量避免在嵌套作用域中暗藏变量以避免混同。
参考如下示例程序:
package main
import "fmt"
func main() {
// 申明并初始化值为 10 的内部变量 'x'。x := 10
fmt.Println("Outer x:", x)
// 在外部作用域里用新变量 'x' 笼罩内部 'x'。if true {
x := 5 // 笼罩产生在这里
fmt.Println("Inner x:", x) // 打印外部 'x', 值为 5
}
// 内部 'x' 放弃不变并依然能够拜访
fmt.Println("Outer x after inner scope:", x) // 打印内部 'x', 值为 10
}
2: 应用接口进行形象
形象
形象是 Go 中的基本概念,容许咱们定义行为而不指定实现细节。
接口
在 Go 语言中,接口是办法签名的汇合。
实现接口的所有办法的任何类型都隐式的满足该接口。
因而只有遵循雷同的接口,咱们就可能编写能够解决不同类型的代码。
上面是一个用 Go 语言编写的示例程序,演示了应用接口进行形象的概念:
package main
import (
"fmt"
"math"
)
// 定义 Shape 接口
type Shape interface {Area() float64
}
// Rectangle 构造
type Rectangle struct {
Width float64
Height float64
}
// Circle 构造
type Circle struct {Radius float64}
// 实现 Rectangle 的 Area 办法
func (r Rectangle) Area() float64 {return r.Width * r.Height}
// 实现 Circle 的 Area 办法
func (c Circle) Area() float64 {return math.Pi * c.Radius * c.Radius}
// 打印任何形态面积的函数
func PrintArea(s Shape) {fmt.Printf("Area: %.2f\n", s.Area())
}
func main() {rectangle := Rectangle{Width: 5, Height: 3}
circle := Circle{Radius: 2.5}
// 对矩形和圆形调用 PrintArea,因为都实现了 Shape 接口
PrintArea(rectangle) // 打印矩形面积
PrintArea(circle) // 打印圆形面积
}
在这个程序中,咱们定义了 Shape
接口,创立了两个构造体 Rectangle
和Circle
,每个构造体都实现了 Area()
办法,并应用 PrintArea
函数打印满足 Shape
接口的任何形态的面积。
这演示了如何在 Go 中应用接口进行形象,从而应用公共接口解决不同的类型。
1: 防止混合库包和可执行文件
在 Go 中,在包和可执行文件之间放弃清晰的隔离以确保代码洁净和可维护性至关重要。
上面是演示库和可执行文件拆散的示例我的项目构造:
myproject/
├── main.go
├── myutils/
└── myutils.go
myutils/myutils.go
:
// 包申明——为实用程序函数创立独自的包
package myutils
import "fmt"
// 导出打印消息的函数
func PrintMessage(message string) {fmt.Println("Message from myutils:", message)
}
main.go
:
// 主程序
package main
import (
"fmt"
"myproject/myutils" // 导入自定义包
)
func main() {
message := "Hello, Golang!"
// 调用自定义包里的导出函数
myutils.PrintMessage(message)
// 演示主程序逻辑
fmt.Println("Message from main:", message)
}
- 在下面的例子中,有两个独立的文件:
myutils.go
和main.go
。 myutils.go
定义了一个名为myutils
的自定义包,蕴含输入函数PrintMessage
,用于打印消息。main.go
是应用相对路径 (myproject/myutils
) 导入自定义包myutils
的可执行文件。main.go
中的main
函数从myutils
包中调用PrintMessage
函数并打印一条音讯。这种关注点拆散放弃了代码的组织性和可维护性。
你好,我是俞凡,在 Motorola 做过研发,当初在 Mavenir 做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI 等技术始终保持着浓重的趣味,平时喜爱浏览、思考,置信继续学习、一生成长,欢送一起交流学习。为了不便大家当前能第一工夫看到文章,请敌人们关注公众号 ”DeepNoMind”,并设个星标吧,如果能一键三连(转发、点赞、在看),则能给我带来更多的反对和能源,激励我继续写下去,和大家独特成长提高!
本文由 mdnice 多平台公布