1. Avoid Using panic
panic
should only be used for truly exceptional situations. Instead, return errors.
Bad Example:
func readFile(filename string) []byte {
data, err := os.ReadFile(filename)
if err != nil {
panic("failed to read file: " + err.Error()) // Improper use of panic
}
return data
}
Good Example:
func readFile(filename string) ([]byte, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("failed to read file: %w", err)
}
return data, nil
}
2. Use Early Returns
Reduce nesting and improve readability by returning early.
Bad Example:
func divide(a, b float64) (float64, error) {
result := 0.0
if b != 0 {
result = a / b
return result, nil
} else {
return 0, errors.New("division by zero")
}
}
Good Example:
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
3. Always Check Errors
Ignoring errors can make debugging difficult.
Bad Example:
func writeFile(filename string, data []byte) {
os.WriteFile(filename, data, 0644) // No error handling
}
Good Example:
func writeFile(filename string, data []byte) error {
err := os.WriteFile(filename, data, 0644)
if err != nil {
return fmt.Errorf("failed to write file: %w", err)
}
return nil
}
4. Avoid Overusing init
Excessive use of init
can lead to hidden side effects.
Bad Example:
var db *sql.DB
func init() {
var err error
db, err = sql.Open("mysql", "user:password@/dbname")
if err != nil {
panic(err) // Panic inside init
}
}
Good Example:
func newDB() (*sql.DB, error) {
db, err := sql.Open("mysql", "user:password@/dbname")
if err != nil {
return nil, fmt.Errorf("failed to connect to DB: %w", err)
}
return db, nil
}
5. Use log
Instead of fmt.Println
for Debugging
Use logging instead of direct print statements.
Bad Example:
func process(data string) {
fmt.Println("Processing:", data) // Debugging print statement
}
Good Example:
func process(data string) {
log.Printf("Processing: %s", data)
}
6. Avoid Overusing interface{}
Using empty interfaces unnecessarily reduces type safety.
Bad Example:
func printValue(v interface{}) {
fmt.Println(v) // Accepts any type without restrictions
}
Good Example:
func printValue[T any](v T) {
fmt.Println(v) // Uses generics
}
7. Use defer
Properly
Ensure that defer
is used correctly for resource management.
Bad Example:
func processFile() {
f, _ := os.Open("file.txt")
defer f.Close() // No error checking
}
Good Example:
func processFile() error {
f, err := os.Open("file.txt")
if err != nil {
return fmt.Errorf("failed to open file: %w", err)
}
defer f.Close()
return nil
}
8. Avoid Capturing Loop Variables in Goroutines
Loop variables should be copied inside the loop to prevent unexpected behavior.
Bad Example:
func process(items []string) {
for _, item := range items {
go func() {
fmt.Println(item) // References shared loop variable
}()
}
}
Good Example:
func process(items []string) {
for _, item := range items {
itemCopy := item
go func() {
fmt.Println(itemCopy) // Uses a copied variable
}()
}
}
9. Avoid time.Sleep
for Synchronization
Use proper synchronization techniques instead of arbitrary delays.
Bad Example:
func worker() {
go func() {
fmt.Println("Working...")
}()
time.Sleep(1 * time.Second) // Arbitrary sleep
}
Good Example:
func worker() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("Working...")
}()
wg.Wait() // Proper synchronization
}
10. Avoid Magic Numbers
Define constants for better readability.
Bad Example:
func calculatePrice(quantity int) int {
return quantity * 100 // 100 is unclear
}
Good Example:
const PricePerUnit = 100
func calculatePrice(quantity int) int {
return quantity * PricePerUnit
}
Following these best practices will help improve the quality, maintainability, and performance of your Go code. Keep these guidelines in mind when writing Go applications.