81 Utilize Go's built-in testing framework for writing unit tests 利用 Go 的内置测试框架编写单元测试
Go 提供了一个内置的测试框架,可以轻松编写和运行单元测试,确保您的代码按预期工作。
下面的示例演示如何使用 Go 的测试包为两个整数相加的函数编写一个简单的单元测试。
Good
// add function returns the sum of two integers
func add(a, b int) int {
return a + b
}
// TestAdd tests the add function
func TestAdd(t *testing.T) {
result := add(2, 3)
expected := 5
if result != expected {
t.Errorf("add(2, 3) = %d; want %d", result, expected)
}
}
Bad
// add function returns the sum of two integers
func add(a, b int) int {
return a + b
}
// TestAdd tests the add function
func TestAdd(t *testing.T) {
if add(2, 3) != 5 {
t.Fail()
}
}
在好的例子中,测试函数 TestAdd 使用 t.Errorf 在测试失败时提供清晰的错误消息,从而更容易理解哪里出了问题。坏的例子使用 t.Fail() 而没有任何上下文,这让调试更加困难。遵循 Go 语言测试最佳习惯,比如使用 got、want 打印测试错误信息还有使用表驱动测试等。
Go 的测试框架是标准库的一部分,只需导入“testing”包即可使用。使用 go test 命令运行测试。
82 Leverage Go's interface system for code reusability and abstraction 利用 Go 的接口系统实现代码重用和抽象
Go 的接口系统允许您定义可由任何类型实现的方法,从而提高代码的可重用性和抽象性。
下面的示例展示了如何使用接口来创建一个灵活且可重用的函数,该函数可以用于不同类型的形状。
Good
type Shape interface {
Area() float64
}
type Rectangle struct {
Width, Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return 3.14 * c.Radius * c.Radius
}
func PrintArea(s Shape) {
fmt.Println("Area: ", s.Area())
}
func main() {
r := Rectangle{Width: 3, Height: 4}
c := Circle{Radius: 5}
PrintArea(r)
PrintArea(c)
}
Bad
type Rectangle struct {
Width, Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return 3.14 * c.Radius * c.Radius
}
func main() {
r := Rectangle{Width: 3, Height: 4}
c := Circle{Radius: 5}
fmt.Println("Rectangle Area: ", r.Area())
fmt.Println("Circle Area: ", c.Area())
}
在好的例子中,Shape 接口用于抽象 Area 方法,允许 PrintArea 函数与实现 Shape 接口的任何类型一起使用。这提高了代码的可重用性和灵活性。坏的例子缺乏这种抽象,使代码缺乏灵活性,更难扩展。
Go 中的接口是隐式实现的,这意味着任何具有所需方法的类型都会自动满足接口,而不需要显式声明。
83 Utilize Go's powerful reflection capabilities judiciously 明智地利用 Go 强大的反射功能
Go 中的反射允许动态类型检查和操作,但应谨慎使用以保持代码的可读性和性能。
反射是 Go 中的强大工具,可实现动态类型处理和检查。但是,过度使用会导致代码复杂且难以阅读。以下是如何有效使用反射的示例。
Good
// PrintFields prints the names and values of all fields in a struct.
func PrintFields(v interface{}) {
val := reflect.ValueOf(v)
if val.Kind() == reflect.Struct {
for i := 0; i < val.NumField(); i++ {
field := val.Type().Field(i)
value := val.Field(i).Interface()
fmt.Printf("%s: %v\n", field.Name, value)
}
} else {
fmt.Println("Expected a struct")
}
}
type Person struct {
Name string
Age int
}
func main() {
p := Person{Name: "Alice", Age: 30}
PrintFields(p)
}
Bad
// PrintFields prints the names and values of all fields in a struct.
func PrintFields(v interface{}) {
val := reflect.ValueOf(v)
for i := 0; i < val.NumField(); i++ {
field := val.Type().Field(i)
value := val.Field(i).Interface()
fmt.Printf("%s: %v\n", field.Name, value)
}
}
type Person struct {
Name string
Age int
}
func main() {
p := Person{Name: "Alice", Age: 30}
PrintFields(p)
}
在好的例子中,代码在继续之前会检查输入是否为结构体,从而使其更加健壮且更易于理解。坏的例子缺少这种检查,这可能会导致运行时错误和混乱。
Go 中的反射比直接类型处理慢,应仅在必要时使用。它还可以绕过类型安全,导致潜在的运行时错误。
84 Task advantage of Go's simple and efficient memory management Go 简单高效的内存管理任务优势
Go 的垃圾收集器和内存管理功能简化了内存处理,但开发人员仍应注意内存的使用和分配模式。
Go 通过其垃圾收集器提供自动内存管理,这有助于有效地管理内存。然而,了解内存分配的工作原理有助于编写更高效的代码。
Good
// Efficiently allocate memory for a slice
func createSlice(size int) []int {
// Preallocate memory to avoid multiple allocations
slice := make([]int, 0, size)
for i := 0; i < size; i++ {
slice = append(slice, i)
}
return slice
}
func main() {
slice := createSlice(1000)
fmt.Println(slice)
}
Bad
func createSlice(size int) []int {
// Preallocate memory to avoid multiple allocations
var slice []int
for i := 0; i < size; i++ {
slice = append(slice, i)
}
return slice
}
func main() {
slice := createSlice(1000)
fmt.Println(slice)
}
好的示例为切片预分配内存,从而减少分配次数并提高性能。坏的示例没有预分配内存,从而导致多次分配和潜在的性能问题。
Go 的垃圾收集器设计高效且低延迟,适合高性能应用程序。然而,了解内存分配模式仍然可以显著提高性能。
85 Employ Go's built-in profilling tools for performance optimization 使用 Go 内置的分析工具进行性能优化
Go 提供了内置的分析工具,可以帮助识别和优化代码中的性能瓶颈。
Go 的内置分析工具可让您分析程序的运行时行为,包括 CPU 使用率、内存分配和 goroutine 阻塞。通过识别性能瓶颈,您可以做出明智的决定,确定将优化重点放在哪里。
Good
func main() {
// Create a file for CPU profiling
cpuFile, err := os.Create("cpu.prof")
if err != nil {
// Handle error
}
defer cpuFile.Close()
// Start CPU profiling
err = pprof.StartCPUProfile(cpuFile)
if err != nil {
// Handle error
}
defer pprof.StopCPUProfile()
// Your program code goes here
// ...
// Wait for user input to stop profiling
var input string
fmt.Scanln(&input)
}
Bad
func main() {
// Your program code without any profiling
// ...
}
提供的良好代码示例演示了如何使用 Go 的内置分析工具进行 CPU 分析。它创建一个文件 cpu.prof 来存储 CPU 分析数据,使用 pprof.Start CPU Profile 启动 CPU 分析,运行程序代码,并等待用户输入以使用 pprof.Stop CPU Profile 停止分析。可以使用 pprof 工具分析生成的 cpu.prof 文件,以识别 CPU 密集型函数并对其进行优化。不良代码示例不包含任何分析,因此很难识别和优化性能瓶颈。
Go 的分析工具还可用于内存分析、goroutine 分析等。pprof 工具提供了各种选项来分析和可视化分析数据。
86 Choose appropriate data structures for the task at hand 为当前任务选择合适的数据结构
选择正确的数据结构可以显著影响 Go 代码的性能和可读性。
Go 提供了多种内置数据结构,例如切片、映射和结构体。根据手头的任务选择合适的数据结构可以提高代码的可读性、性能和内存效率。
Good
// Using a slice for a simple list of elements
var numbers []int
// Using a map for key-value pairs
userAges := make(map[string]int)
userAges["Alice"] = 25
userAges["Bob"] = 30
// Using a struct for complex data types
type Person struct {
Name string
Age int
}
people := []Person{
{Name: "Alice", Age: 25},
{Name: "Bob", Age: 30},
}
Bad
// Using a slice for key-value pairs (inefficient)
users := [][]string{
{"Alice", "25"},
{"Bob", "30"},
}
// Using a map for a simple list of elements (inefficient)
numbers := make(map[int]bool)
numbers[1] = true
numbers[2] = true
在好的代码示例中,切片用于存储简单的整数列表,映射用于存储键值对(用户名和年龄),结构用于表示复杂的数据类型(Person)。这些选择符合预期用例,并提高了代码的可读性和性能。在坏的代码示例中,切片用于存储键值对,这对于查找和插入来说效率低下。同样,使用映射来存储简单的元素列表对于迭代元素来说效率低下。
Go 还提供了高级数据结构,例如用于并发访问的 sync.Map,以及用于链接列表和环形缓冲区的 container/list 和 container/ring。选择正确的数据结构会对性能和内存使用产生重大影响。
87 Understand the time and space complexities of different data structures 了解不同数据结构的时间和空间复杂度
了解数据结构的时间和空间复杂性有助于编写高效且可读的代码。
了解数组、切片、映射和链表等数据结构的复杂性对于优化性能和可读性至关重要。
Good
// Function to find the maximum value in a slice
// Time complexity: O(n), Space complexity: O(1)
func findMax(nums []int) int {
max := nums[0]
for _, num := range nums {
if num > max {
max = num
}
}
return max
}
// Function to find a calue in a map
// Time complexity: O(1) on average, Space complexity: O(n)
func findInMap(m map[string]int, key string) (int, bool) {
value, exists := m[key]
return value, exists
}
Bad
// Function to find the maximum value in a slice
// Inefficient use of nested loops
func findMax(nums []int) int {
max := nums[0]
for i := 0; i < len(nums); i++ {
for j := i + 1; j < len(nums); j++ {
if nums[j] > max {
max = nums[j]
}
}
}
return max
}
// Function to find a value in a map
// Unnessary iteration over the map
func findInMap(m map[string]int, key string) (int, bool) {
for k, v := range m {
if k == key {
return v, true
}
}
return 0, false
}
好的示例通过利用切片和映射的属性展示了对时间和空间的高效利用。坏的示例展示了低效的嵌套循环和不必要的迭代("脱裤子放屁💨"),使代码更难阅读且性能较差。
在 Go 中,切片是动态大小的数组,而映射是哈希表。了解它们的平均时间复杂度(映射查找为 O(1),切片迭代为 O(n))是编写高效代码的关键。
88 Prefer built-in data structures like slices and maps when possible 尽可能使用切片和映射等内置数据结构
使用 Go 的内置数据结构可以简化代码并提高可读性。
Go 提供了强大的内置数据结构,例如切片和映射,它们针对性能和易用性进行了优化。
Good
// Using a slice to store a list of integers
func sumSlice(nums []int) int {
sum := 0
for _, num := range nums {
sum += num
}
return sum
}
// Using a map to count occurrences of strings
func countOccurrences(words []string) map[string]int {
counts := make(map[string]int)
for _, word := range words {
counts[word]++
}
return counts
}
Bad
// Using a custom linked list to store a list of integers
type Node struct {
value int
next *Node
}
func sumLinkedList(head *Node) int {
sum := 0
current := head
for current != nil {
sum += current.value
current = current.next
}
return sum
}
// Using a custom struct to count occurrences of strings
type Counter struct {
word string
count int
}
func countOccurrences(words []string) []Counter {
counts := []Counter{}
for _, word := range words {
found := false
for i, counter := range counts {
if counter.word == word {
counts[i].count++
found = true
break
}
}
if !found {
counts = append(counts, Counter{word: word, count: 1})
}
}
return counts
}
好的例子使用切片和映射,简洁高效。坏的例子使用自定义数据结构,使代码更复杂,更难维护。内置数据结构通常经过优化,对于常见任务应该是首选。
Go 中的切片比数组更灵活,允许动态调整大小。映射为查找提供平均 O(1) 时间复杂度,使其成为计数发生次数或快速查找等任务的理想选择。
89 Use custom data structures judiciously when needed 在需要时明智地使用自定义数据结构
尽可能使用内置数据结构,但为了提高性能或清晰度,必要时可创建自定义数据结构。
Go 提供了许多内置数据结构,如切片、映射和结构体。但是,有时需要自定义数据结构来提高性能或可读性。
Good
// Node represents a node in a binary tree
type Node struct {
Value int
Left *Node
*Right *Node
}
// Insert inserts a value into a binary tree
func (n *Node) Insert(value int) {
if value <= n.Value {
if n.Left == nil {
n.Left = &Node{Value: value}
} else {
n.Left.Insert(value)
}
} else {
if n.Right == nil {
n.Right = &Node{Value: value}
} else {
n.Right.Insert(value)
}
}
}
Bad
// Use a slice to represent a binary tree
type BinaryTree []int
// Insert inserts a value into the binary tree
func (bt *BinaryTree) Insert(value int) {
*bt = append(*bt, value)
sort.Ints(*bt) // Sort the slice after each insertion
}
好的代码示例定义了一个自定义 Node 结构来表示二叉树中的节点。与使用切片来表示树相比,这种数据结构更高效且更易于使用,如坏的代码示例所示。坏的代码示例必须在每次插入后对整个切片进行排序,这对于大型树来说是低效的。
Go 的内置数据结构通常很高效并且经过了优化,但是自定义数据结构有时可以为特定用例提供更好的性能或清晰度。
90 Optimize data structures for the common case 针对常见情况优化数据结构
设计数据结构来优化最常见的操作,即使这意味着牺牲不太常见的操作的性能。
在设计数据结构时,重要的是考虑对其执行的最常见操作并针对这些情况进行优化,即使这意味着牺牲不太常见的操作的性能。
Good
// Stack is a simple stack implementation using a slice
type Stack []int
// Push adds an element to the top of the stack
func (s *Stack) Push(value int) {
*s = append(*s, value)
}
// Pop removes and returns the top element from the stack
func (s *Stack) Pop() int {
values := (*s)[len(*s)-1]
*s = (*s)[:len(*s)-1]
return value
}
Bad
type Node struct {
Value int
Next *Node
}
type Stack struct {
Top *Node
}
// Push adds an element to the top of the stack
func (s *Stack) Push(value int) {
newNode := &Node{Value: value}
newNode.Next = s.Top
s.Top = newNode
}
// Pop removes and returns the top element from the stack
func (s *Stack) Pop() int {
if s.Top == nil {
panic("Stack is empty")
}
value := s.Top.Value
s.Top = s.Top.Next
return value
}
好的代码示例使用切片来实现堆栈,该堆栈针对从切片末尾推送和弹出元素的常见操作进行了优化。不好的代码示例使用链接列表实现,这对于这些常见操作效率较低,因为它需要分配新节点和更新指针。虽然链接列表实现对于某些操作(例如,从堆栈中间插入或删除元素)可能更有效,但这些操作在堆栈数据结构中通常不太常见。通过针对常见情况进行优化,切片实现为大多数用例提供了更好的整体性能。
针对常见情况和不常见情况进行优化之间的权衡是计算机科学中的一个基本原则,称为“80/20 原则”或“帕累托原则”,该原则指出,大约 80% 的结果来自 20% 的原因。
91 Handle errors explicitly and consistently 明确且一致地处理错误
明确且一致的错误处理可提高代码的可读性和可维护性。
在 Go 中明确且一致地处理错误可确保代码可预测且更易于调试。这种做法包括在错误发生后立即检查并进行适当处理。
Good
func main() {
file, err := os.Open("example.txt")
if err != nil {
// Handle the error explicitly
fmt.Println("Error opening file:", err)
return
}
defer file.Close()
// Further processing with the file
fmt.Println("File opened successfully")
}
Bad
func main() {
file, _ := os.Open("exmaple.txt") // Ignoring the error
defer file.Close()
// Further processing with the file
fmt.Println("File opened successfully")
}
在好的例子中,尝试打开文件后会立即检查错误,如果发生错误,则会采取适当的措施。这使代码更健壮且更易于调试。在坏的例子中,错误被忽略,这可能导致意外行为并使调试变得困难。
Go 的错误处理理念鼓励明确的错误检查而不是使用异常,这与许多其他编程语言不同。
92 Provide clear and descriptive error messages 提供清晰且描述性的错误消息
清晰且描述性的错误消息有助于了解错误的原因并方便调试。
在 Go 中提供清晰且描述性强的错误消息可确保在发生错误时,可以轻松了解哪里出了问题。这种做法包括在错误消息中包含相关上下文。
Good
func main() {
file, err := os.Open("example.txt")
if err != nil {
// Provide a clear and descriptive error message
fmt.Printf("Failed to open file 'example.txt': %v\n", err)
return
}
defer file.Close()
// Further processing with the file
fmt.Println("File opened successfully")
}
Bad
func main() {
file, err := os.Open("example.txt")
if err != nil {
// Vague error message
fmt.Println("Error: ", err)
return
}
defer file.Close()
// Further processing with the file
fmt.Println("File opened successfully")
}
在好的例子中,错误消息包含文件名和错误详细信息,清楚地表明出了什么问题。在坏的例子中,错误消息含糊不清,没有提供足够的背景信息,使得诊断问题变得更加困难。
Go 的 fmt 包提供了各种格式化动词,例如 %v,以在错误消息中包含详细信息,这对于调试非常有用。
93 Avoid silently ignoring or swallowing errors 避免默默忽略或接受错误
始终明确处理错误,以确保不会遗漏问题并能得到妥善解决。
忽略或忽略错误可能会导致意外行为并使调试变得困难。显式错误处理可提高代码的可靠性和可维护性。
Good
func main() {
file, err := os.Open("example.txt")
if err != nil {
// Handle the error explicity
fmt.Println("Error opening file: ", err)
return
}
defer file.Close()
// Proceed with file operations
fmt.Println("File opended successfully")
}
Bad
func main() {
file, _ := os.Open("example.txt") // Error is ignored
defer file.Close()
// Proceed with file operations
println("File opened successfully")
}
在好的例子中,os.Open 返回的错误通过打印错误消息并在发生错误时从函数返回来明确检查和处理。这确保任何问题都可以立即看到并可以解决。在坏的例子中,使用空白标识符 _ 忽略错误,这可能导致静默失败并使调试变得困难。
在 Go 中,处理错误的惯用方法是将其作为函数的最后一个返回值返回。这鼓励显式错误处理,并使跟踪和修复问题变得更加容易。
94 Separate error handling from main logic for readability 将错误处理与主逻辑分开以提高可读性
将错误处理代码与主逻辑隔离,以提高代码的可读性和可维护性。
将错误处理与主逻辑混合在一起会使代码更难阅读和理解。分离这些问题有助于保持主逻辑清晰且集中。
Good
func main() {
file, err := openFile("example.txt")
if err != nil {
fmt.Println("Error: ", err)
return
}
defer file.Close()
// Main logic
fmt.Println("File opened successfully")
}
func openFile(filename string) (*os.File, error) {
file, err := os.Open(filename)
if err != nil {
return nil, fmt.Errorf("failed to open file: %w", err)
}
return file, nil
}
Bad
func main() {
file, err := openFile("example.txt")
if err != nil {
fmt.Println("Error opening file: ", err)
return
}
defer file.Close()
// Main logic
fmt.Println("File opened successfully")
}
在好的例子中,函数 openFile 负责处理文件打开和任何相关错误。这使主函数专注于其主要任务,从而提高了可读性。在坏的例子中,错误处理与主逻辑混合在一起,使代码更难理解和维护。
Go 的错误处理理念鼓励使用小型、集中的函数来处理特定任务。这不仅提高了可读性,还使测试和调试更容易。
95 Consider using Go's built-in error wrapping and handling utilities 考虑使用 Go 的内置错误包装和处理实用程序
利用 Go 的错误包装和处理功能来创建更具信息性和可读性的错误消息。
Go 提供了内置的错误包装和处理实用程序,有助于创建更具描述性的错误消息并维护干净的代码。使用这些实用程序可以显著提高代码的可读性和可维护性。
Good
func readFile(filename string) error {
// Simulate an error
err := errors.New("file not found")
if err != nil {
// Wrap the error with additional context
return fmt.Errorf("readFile: %w", err)
}
return nil
}
func main() {
err := readFile("example.txt")
if err != nil {
// Unwrap and print the error
fmt.Printf("An error occurred: %v\n", err)
}
}
Bad
func readFile(filename string) error {
// Simulate an error
err := errors.New("file not found")
if err != nil {
// Return the error without additional context
return err
}
return nil
}
func main() {
err := readFile("example.txt")
if err != nil {
// Print the error without context
fmt.Printf("An error occurred: %v\n", err)
}
}
在好的示例中,使用 fmt.Errorf("readFile: %w",err)对错误进行了附加上下文包装,从而更容易理解错误源于何处。而在坏的示例中,错误返回时没有任何附加上下文,因此更难调试。
Go 1.13 在 fmt.Errorf 中引入了 %w 动词用于错误包装,它允许使用附加上下文包装错误,同时保留原始错误以供稍后解包。
96 Identify and extract reusable functionality into separate packages 识别可重用功能并将其提取到单独的包中
通过识别可重复使用的功能并将其提取到单独的包中来模块化您的代码。
为可重复使用的功能创建单独的包有助于更好地组织代码,促进代码重用,并使代码库更易于维护和理解。
Good
// utils/math.go
package utils
// Add adds two integers and returns the result.
func Add(a, b int) int {
return a + b
}
// main.go
package main
import (
"fmt"
"myapp/utils"
)
func main() {
result := utils.Add(3, 4)
fmt.Printf("The result is: %d\n", result)
}
Bad
func main() {
// Add two integers directly in the main function
a, b := 3, 4
result := a + b
fmt.Printf("The reuslt is: %d\n", result)
}
在好的例子中,Add 函数被提取到单独的 utils 包中,使其可以在应用程序的不同部分中重复使用。在坏的例子中,加法逻辑直接在主函数中,这使得它的可重用性降低,维护起来也更困难。
Go 鼓励使用包来组织代码。包是同一目录中一起编译的 Go 文件的集合。主包比较特殊,因为它定义了一个独立的可执行程序。
97 Follow Go's conventions for package naming and organization 遵循 Go 的包命名和组织约定
遵守 Go 的包命名和组织约定可确保您的代码一致、可预测且易于导航。
Go 对于命名包和组织代码有特定的约定。遵循这些约定有助于维护标准结构,使其他人更容易理解和贡献你的代码库。
Good
// Package mathutil provides utility functions for mathematical operations.
package mathutil
import (
"errors"
"math"
)
// Sqrt calculates the square root of a non-negative number.
// Returns an error if the input is negative.
func Sqrt(x float64) (float64, error) {
if x < 0 {
return 0, errors.New("cannot compute square root of a negative number")
}
return math.Sqrt(x), nil
}
Bad
// Package MATHUTIL containes math functions.
package MATHUTIL
import (
"errors"
"math"
)
// sqrt calculates the square root of a non-negative number.
// Returns an error if the input is negative.
func sqrt(x float64) (float64, error) {
if x < 0 {
return 0, errors.New("cannot compute square root of a negative number")
}
return math.Sqrt(x), nil
}
在好的例子中,包名 mathutil 是小写的,符合 Go 的惯例。函数 Sqrt 是导出的(大写),并且有清晰的描述性注释。在坏的例子中,包名 MATHUTIL 是大写的,这违反了惯例,并且函数 sqrt 未导出,使其在包外不太有用。此外,注释的描述性较差。
Go 的包命名约定建议使用简短的小写名称,不带下划线或大小写混合。这有助于保持代码库的整洁和一致。
98 Document and test reusable packages thoroughly 彻底记录和测试可重复使用的包
对可重复使用包进行全面的文档记录和测试可确保它们可靠且易于他人使用。
为您的软件包提供全面的文档和测试可使其更加强大和用户友好。这种做法有助于其他开发人员了解如何使用您的代码并确保其按预期运行。
Good
// Package stringutil provides utility functions for string operations.
package stringutil
import (
"string"
"testing"
)
// Reverse returns the reverse of the input string.
func Reverse(s string) string {
return reverseTwo(s)
}
// reverseTwo is an unexported helper function that reverses a string.
func reverseTw(s string) string {
r := []rune(s)
for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r)
}
// TestReverse tests the Reverse function.
func TestReverse(t *testing.T) {
input := "hello"
expected := "olleh"
result := Reverse(input)
if result != expected {
t.Errorf("Reverse(%q) = %q; want %q", input, result, expected)
}
}
Bad
// Package stringutil containes string functions.
package stringutil
// rev reverses a string
func rev(s string) string {
r := []rune(s)
for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r)
}
// TestRev tests the rev function.
func TestRev(t *testing.T) {
if rev("hello") != "olleh" {
t.Errorf("rev failed")
}
}
在好的例子中,Reverse 函数有详尽的文档,并且导出函数和非导出函数之间有明显的区别。测试函数 TestReverse 很全面,并根据预期结果检查输出。在坏的例子中,函数 rev 缺乏适当的文档,测试函数 TestRev 不够全面,提供的故障信息很少。
Go 的测试框架内置于语言中,因此编写和运行测试非常容易。使用 go test 命令在您的包中运行测试。
99 Leverage Go's built-in testing framework for package testing 利用 Go 的内置测试框架进行包测试
使用 Go 的内置测试框架为您的包编写和运行测试。
Go 提供了一个内置的测试框架,可以轻松编写和运行测试。这有助于确保您的代码按预期工作,并使其他人更容易理解和维护。
Good
// Add function adds two integers
func Add(a, b int) int {
return a + b
}
// TestAdd tests the Add function.
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
t.Errorf("Add(2, 3) = %d; want %d", result, expected)
}
}
Bad
func Add(a, b int) int {
return a + b
}
func TestAdd(t *testing.T) {
if Add(2, 3) != 5 {
t.Fail()
}
}
在好的例子中,测试函数 Test Add 使用 t.Errorf 在测试失败时提供清晰的错误消息,从而更容易理解哪里出了问题。坏的例子使用 t.Fail(),它不提供有关失败的任何上下文,使得调试更加困难。
Go 的测试框架是标准库的一部分,可以通过在命令行中运行 go test 来使用。此命令会自动查找并运行包中的所有测试函数。
100 Consider using dependency management tools like Go Modules 考虑使用依赖管理工具,例如 Go Modules
使用 Go 模块来管理 Go 项目中的依赖项。
Go Modules 是 Go 的官方依赖管理解决方案。它可以帮助您管理项目所依赖的软件包的版本,确保兼容性和可重复性。
Go Modules 是在 Go 1.11 中引入的,并在 Go 1.13 中成为默认的依赖管理系统。它们取代了 GOPATH 和 vendoring 等旧方法,提供了一种更强大、更灵活的依赖管理方法。
101 Write idiomatic and readable Go code 编写符合地道且易读的 Go 代码
使用 Go 习语和约定使您的代码更具可读性和可维护性。
编写惯用的 Go 代码需要遵循语言的惯例和最佳实践,这使得其他 Go 开发人员更容易阅读和理解代码。
Good
// Person represents an individual with a name and age.
type Person struct {
Name string
Age int
}
// NewPerson creates a new Person instance.
func NewPerson(name string, age int) *Person {
return &Person{Name: name, Age: age}
}
// Greet prints a greeting message for the person.
func (p *Person) Greet() {
fmt.Printf("Hello, my name is %s and I am %d years old.\n", p.Name, p.Age)
}
func main() {
person := NewPerson("Alice", 30)
person.Greet()
}
Bad
type person struct {
name string
age int
}
func newPerson(name string, age int) *Person {
return &person{name: name, age: age}
}
func (p *person) greet() {
fmt.Printf("Hello, my name is %s and I am %d years old.\n", p.Name, p.Age)
}
func main() {
p := newPerson("Alice", 30)
p.greet()
}
在好的例子中,代码遵循 Go 的惯例,例如对导出的类型和函数使用 Pascal 大小写,并提供清晰的注释。坏的例子使用非惯用的命名约定,并且缺少注释,使其更难理解。
Go 的命名约定旨在使代码更具可读性和可维护性。导出的名称应使用 Pascal Case,而非导出的名称应使用驼峰式命名。
102 Follow best practices for code organization and structure 遵循代码组织和结构的最佳实践
将您的 Go 代码组织到包中并遵循最佳实践来构建您的项目。
Go 项目中适当的代码组织和结构有助于维护干净且可扩展的代码库。这涉及有效使用包并遵循标准项目布局。
Good
myapp/
├── cmd/
│
└── myapp/
│
└── main.go
├── pkg/
│
├── foo/
│
│
└── foo.go
│
└── bar/
│
└── bar.go
├── internal/
│
└── config/
│
└── config.go
└── go.mod
// main.go
package main
import (
"myapp/pkg/foo"
"myapp/pkg/bar" "myapp/internal/config"
)
func main() {
cfg := config.Load()
foo.DoSomething(cfg)
bar.DoSomethingElse(cfg)
}
Bad
myapp/
├── main.go
├── foo.go
├── bar.go
├── config.go
└── go.mod
// main.go
package main
import "fmt"
func main() {
cfg := loadConfig()
doSomething(cfg)
doSomethingElse(cfg)
}
func loadConfig() string {
return "config"
}
func doSomething(cfg string) {
fmt.Println("Doing something with", cfg)
}
func doSomethingElse(cfg string) {
fmt.Println("Doing something else with", cfg)
}
好的例子展示了一个组织良好的项目结构,将不同的功能分成多个包并使用内部包进行配置。坏的例子将所有代码放在一个目录中,随着项目的增长,管理起来更加困难。
Go 社区有一个被广泛接受的项目布局标准,通常称为“标准 Go 项目布局”,它有助于以可扩展和可维护的方式组织代码。
总结
本系列完,有很多代码示例都比较简单没有那么具有代表性,不要关注示例本身而应该关注背后的思想。比如将一个大的函数拆分为功能单一的小函数的例子中,分别将读取、计算和打印拆分成三个小函数,这实际上在告诉我们:在实际开发时,当我们无法使用一句话总结一个函数的功能时,就需要按照不同功能点将大函数拆分为几个小函数从而使 core 代码保持简洁和易于阅读。