# Security Best Practices Standards for Go
This document outlines security best practices for Go development, serving as a guide for developers and a context source for AI coding assistants. The goal is to promote secure coding patterns and protect against common vulnerabilities in Go applications.
## 1. Input Validation and Sanitization
### 1.1. Overview
Input validation is a critical first line of defense against various attacks, including Cross-Site Scripting (XSS), SQL Injection, and command injection. Sanitization ensures that input conforms to expected formats, preventing malicious data from being processed by the system.
### 1.2. Standards
* **Do This:** Validate all inputs, including those from users, external services, and configuration files.
* **Do This:** Use allow-lists instead of block-lists for validation where feasible. Define what *is* allowed rather than attempting to block everything that *isn't*.
* **Do This:** Sanitize inputs to remove or escape potentially harmful characters.
* **Don't Do This:** Rely solely on client-side validation. Always validate on the server-side.
* **Don't Do This:** Blindly trust data from external sources.
### 1.3. Rationale
Untrusted input can be a gateway for attackers to inject malicious code or data into your application. Comprehensive, server-side input validation and sanitization are essential for mitigating this risk.
### 1.4. Code Examples
#### 1.4.1. Validating Email Address
"""go
package main
import (
"fmt"
"net/mail"
"regexp"
)
// isValidEmail checks if the provided string is a valid email address
func isValidEmail(email string) bool {
_, err := mail.ParseAddress(email)
return err == nil
}
// sanitizeString removes potentially harmful characters from a string
func sanitizeString(input string) string {
re := regexp.MustCompile("[^a-zA-Z0-9@._-]") // Example: Allow only alphanumeric chars, @, ., _, and -
return re.ReplaceAllString(input, "")
}
func main() {
email1 := "test@example.com"
email2 := "invalid-email"
email3 := "@example.com" //Potential XSS
if isValidEmail(email1) {
fmt.Println(email1, "is a valid email")
} else {
fmt.Println(email1, "is an invalid email")
}
if isValidEmail(email2) {
fmt.Println(email2, "is a valid email")
} else {
fmt.Println(email2, "is an invalid email")
}
if isValidEmail(email3) {
sanitizedEmail := sanitizeString(email3)
fmt.Printf("Original email '%s' is valid (after sanitization: '%s')\n", email3, sanitizedEmail)
} else {
fmt.Printf("Original email '%s' is invalid.\n", email3)
}
}
"""
#### 1.4.2. Validating Integer Input
"""go
package main
import (
"fmt"
"strconv"
)
// validateInteger checks if the input string is a valid integer within a specified range.
func validateInteger(input string, min, max int) (int, error) {
num, err := strconv.Atoi(input)
if err != nil {
return 0, fmt.Errorf("invalid integer: %w", err)
}
if num < min || num > max {
return 0, fmt.Errorf("integer out of range (min: %d, max: %d)", min, max)
}
return num, nil
}
func main() {
input1 := "123"
input2 := "abc"
input3 := "500" // Out of range
num1, err1 := validateInteger(input1, 1, 200)
if err1 != nil {
fmt.Println("Error:", err1)
} else {
fmt.Println("Valid integer:", num1)
}
num2, err2 := validateInteger(input2, 1, 200)
if err2 != nil {
fmt.Println("Error:", err2)
} else {
fmt.Println("Valid integer:", num2)
}
num3, err3 := validateInteger(input3, 1, 200)
if err3 != nil {
fmt.Println("Error:", err3)
} else {
fmt.Println("Valid integer:", num3)
}
}
"""
### 1.5. Anti-Patterns
* Failing to validate input length can lead to buffer overflows, especially when interacting with C libraries via "cgo".
* Using regular expressions without understanding their complexity can lead to ReDoS (Regular expression Denial of Service) attacks.
* Ignoring errors returned by validation functions.
## 2. Preventing Injection Attacks
### 2.1. Overview
Injection attacks exploit vulnerabilities where untrusted data is sent to an interpreter as part of a command or query. Common types include SQL Injection, Command Injection, and LDAP Injection.
### 2.2. Standards
* **Do This:** Use parameterized queries or prepared statements for database interactions.
* **Do This:** Avoid constructing SQL queries by concatenating strings.
* **Do This:** Escape special characters when interpolating data into commands or scripts.
* **Do This:** When executing external commands, use functions that prevent shell injection.
* **Don't Do This:** Directly concatenate user input into SQL queries.
* **Don't Do This:** Use "os/exec" without proper input sanitization.
* **Don't Do This:** Pass unsanitized input to template engines.
### 2.3. Rationale
Parameterized queries ensure that user-provided data is treated as data, not as part of the SQL query's structure. This prevents attackers from injecting malicious SQL code. Similar principles apply to preventing command injection in operating system calls.
### 2.4. Code Examples
#### 2.4.1. Using Parameterized Queries with "database/sql"
"""go
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/lib/pq" // PostgreSQL driver
)
func main() {
db, err := sql.Open("postgres", "user=postgres password=password dbname=mydb sslmode=disable")
if err != nil {
log.Fatal(err)
}
defer db.Close()
username := "test' OR '1'='1" // Malicious input example
// BAD: Vulnerable to SQL injection
// query := "SELECT * FROM users WHERE username = '" + username + "'"
// rows, err := db.Query(query)
// GOOD: Using parameterized query
query := "SELECT * FROM users WHERE username = $1"
rows, err := db.Query(query, username)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var id int
var username string
if err := rows.Scan(&id, &username); err != nil {
log.Fatal(err)
}
fmt.Printf("ID: %d, Username: %s\n", id, username)
}
if err := rows.Err(); err != nil {
log.Fatal(err)
}
}
"""
#### 2.4.2. Preventing Command Injection with "exec.Command"
"""go
package main
import (
"fmt"
"log"
"os/exec"
"strings"
)
// sanitizeFilename removes characters that could be used for command injection.
func sanitizeFilename(filename string) string {
re := strings.NewReplacer(";", "", "&", "", "|", "", """, "", "$", "", "(", "", ")", "")
return re.Replace(filename)
}
func main() {
userInput := "file.txt; rm -rf /" // Malicious input
// BAD: Vulnerable to command injection
// cmd := exec.Command("ls", userInput)
// output, err := cmd.Output()
// GOOD: Using exec.Command correctly with sanitized input.
sanitizedInput := sanitizeFilename(userInput)
cmd := exec.Command("ls", sanitizedInput)
output, err := cmd.Output()
if err != nil {
log.Printf("Error: %v\n", err) // Log the error, don't expose directly to user
}
fmt.Println(string(output))
}
"""
### 2.5. Anti-Patterns
* Assuming that escaping a few characters is sufficient protection.
* Using shell commands when Go provides native libraries for the same functionality.
* Ignoring the return code of external commands, which can indicate failure or malicious activity.
## 3. Authentication and Authorization
### 3.1. Overview
Authentication verifies the identity of a user, while authorization determines what resources they are allowed to access. Robust authentication and authorization mechanisms are crucial for protecting sensitive data and functionality.
### 3.2. Standards
* **Do This:** Use strong password hashing algorithms like bcrypt, scrypt or Argon2.
* **Do This:** Implement proper session management with appropriate timeouts and secure cookies.
* **Do This:** Enforce the principle of least privilege.
* **Do This:** Utilize established authentication and authorization libraries whenever possible.
* **Do This:** Implement multi-factor authentication (MFA) where possible.
* **Don't Do This:** Store passwords in plain text or using weak hashing algorithms like MD5 or SHA1.
* **Don't Do This:** Rely solely on cookies for authentication without proper session management.
* **Don't Do This:** Grant excessive permissions to users or roles.
### 3.3. Rationale
Weak authentication makes it easy for attackers to gain unauthorized access. Insufficient authorization allows attackers to perform actions they shouldn't.
### 3.4. Code Examples
#### 3.4.1. Password Hashing with "bcrypt"
"""go
package main
import (
"fmt"
"log"
"golang.org/x/crypto/bcrypt"
)
// hashPassword hashes the password using bcrypt.
func hashPassword(password string) (string, error) {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", fmt.Errorf("failed to hash password: %w", err)
}
return string(hashedPassword), nil
}
// verifyPassword compares the provided password with the stored hash.
func verifyPassword(password string, hashedPassword string) error {
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
return err
}
func main() {
password := "securePassword123"
hashedPassword, err := hashPassword(password)
if err != nil {
log.Fatal(err)
}
fmt.Println("Hashed password:", hashedPassword)
err = verifyPassword(password, hashedPassword)
if err == nil {
fmt.Println("Password verified successfully.")
} else {
fmt.Println("Password verification failed:", err)
}
}
"""
#### 3.4.2. Session Management with Secure Cookies (Example using "gorilla/sessions")
"""go
package main
import (
"fmt"
"log"
"net/http"
"github.com/gorilla/sessions"
)
var (
// Key must be 32 bytes for AES
key = []byte("super-secret-key-that-should-be-changed") // Replace with a strong, randomly generated key
store = sessions.NewCookieStore(key)
)
func secret(w http.ResponseWriter, r *http.Request) {
session, _ := store.Get(r, "session-name")
// Check if user is authenticated
if auth, ok := session.Values["authenticated"].(bool); !ok || !auth {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
// Print secret message
fmt.Fprintln(w, "The secret message is: You're logged in!")
}
func login(w http.ResponseWriter, r *http.Request) {
session, _ := store.Get(r, "session-name")
// Authentication would happen here (e.g., verifying username and password)
// For this example, we'll just set the authenticated flag to true
session.Values["authenticated"] = true
session.Save(r, w)
fmt.Fprintln(w, "Logged in!")
}
func logout(w http.ResponseWriter, r *http.Request) {
session, _ := store.Get(r, "session-name")
session.Options.MaxAge = -1 // Clear session
session.Save(r, w)
fmt.Fprintln(w, "Logged out!")
}
func main() {
store.Options = &sessions.Options{
Path: "/",
MaxAge: 86400 * 30, // 30 days
HttpOnly: true, // Mitigate XSS attacks
Secure: true, //only send cookie over HTTPS (Set to false during local development if not using HTTPS)
}
http.HandleFunc("/secret", secret)
http.HandleFunc("/login", login)
http.HandleFunc("/logout", logout)
fmt.Println("Server listening on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
"""
### 3.5. Anti-Patterns
* Using predictable or easily guessable session IDs.
* Storing sensitive information in cookies without encryption.
* Failing to invalidate sessions on logout or password change.
* Not setting the "Secure" flag on cookies in production environments that are using HTTPS, leading to session hijacking possibilities over HTTP.
## 4. Secure Communication (TLS/SSL)
### 4.1. Overview
Transport Layer Security (TLS) and its predecessor Secure Sockets Layer (SSL) encrypt communication between a client and a server, protecting data from eavesdropping and tampering.
### 4.2. Standards
* **Do This:** Use TLS for all sensitive communications.
* **Do This:** Enforce HTTPS by redirecting all HTTP requests to HTTPS.
* **Do This:** Use strong cipher suites and TLS versions.
* **Do This:** Regularly update TLS certificates.
* **Don't Do This:** Use self-signed certificates in production.
* **Don't Do This:** Use outdated SSL/TLS protocols like SSLv3 or TLS 1.0.
### 4.3. Rationale
TLS encrypts data in transit, preventing attackers from intercepting sensitive information like passwords, credit card numbers, and personal data.
### 4.4. Code Examples
#### 4.4.1. Configuring HTTPS Server
"""go
package main
import (
"fmt"
"log"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, HTTPS!")
}
func main() {
http.HandleFunc("/", handler)
// Start HTTPS server. Ensure you have cert.pem and key.pem files.
fmt.Println("Server listening on :443")
log.Fatal(http.ListenAndServeTLS(":443", "cert.pem", "key.pem", nil))
}
"""
#### 4.4.2. Redirecting HTTP to HTTPS
"""go
package main
import (
"fmt"
"log"
"net/http"
)
func redirectHTTPS(w http.ResponseWriter, r *http.Request) {
target := "https://" + r.Host + r.URL.Path
if len(r.URL.RawQuery) > 0 {
target += "?" + r.URL.RawQuery
}
http.Redirect(w, r, target, http.StatusPermanentRedirect)
}
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, HTTPS!")
}
func main() {
http.HandleFunc("/", handler)
// HTTPS server
go func() {
fmt.Println("HTTPS server listening on :443")
log.Fatal(http.ListenAndServeTLS(":443", "cert.pem", "key.pem", nil))
}()
// HTTP redirect server
go func() {
fmt.Println("HTTP redirect server listening on :80")
log.Fatal(http.ListenAndServe(":80", http.HandlerFunc(redirectHTTPS)))
}()
select {} // Keep the program running
}
"""
### 4.5. Anti-Patterns
* Using weak or default TLS configurations.
* Failing to validate the server's certificate.
* Exposing sensitive services over HTTP.
* Not keeping TLS certificates up-to-date.
## 5. Error Handling and Logging
### 5.1. Overview
Proper error handling prevents unexpected application crashes and data corruption. Comprehensive logging provides valuable information for debugging issues, detecting security breaches, and monitoring system performance.
### 5.2. Standards
* **Do This:** Always check and handle errors returned by functions.
* **Do This:** Log errors with sufficient context for debugging.
* **Do This:** Use structured logging formats (e.g., JSON) for easier analysis.
* **Do This:** Protect sensitive information from being logged (e.g., passwords, API keys).
* **Don't Do This:** Ignore errors without handling them.
* **Don't Do This:** Log sensitive data in plain text.
* **Don't Do This:** Expose detailed error messages to end-users.
### 5.3. Rationale
Unhandled errors can lead to unpredictable behavior and security vulnerabilities. Poor logging makes it difficult to diagnose and resolve issues. Leaked sensitive information in logs can compromise security.
### 5.4. Code Examples
#### 5.4.1. Proper Error Handling
"""go
package main
import (
"fmt"
"log"
"os"
)
func readFile(filename string) (string, error) {
content, err := os.ReadFile(filename)
if err != nil {
return "", fmt.Errorf("failed to read file %s: %w", filename, err)
}
return string(content), nil
}
func main() {
content, err := readFile("myfile.txt")
if err != nil {
log.Printf("Error: %v", err) // Log the error, but don't expose to user
fmt.Println("An error occurred processing the request. Please check the logs.") // User-friendly error message.
os.Exit(1) // Exit gracefully
}
fmt.Println("File content:", content)
}
"""
#### 5.4.2. Structured Logging with "log/slog" (Go 1.21+)
"""go
package main
import (
"log/slog"
"os"
)
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
slog.SetDefault(logger) //Make the logger easy to use globally
filename := "important_config.json"
_, err := os.ReadFile(filename)
if err != nil {
slog.Error("Failed to read configuration file", "filename", filename, "error", err)
} else {
slog.Info("Configuration file read successfully", "filename", filename)
}
// Logging with attributes
slog.Info("User login attempt", slog.String("username", "johndoe"), slog.Bool("successful", false))
// Example of logging a potentially sensitive value (hash instead in reality).
apiKey := "YOUR_ACTUAL_API_KEY"
redactedApiKey := "xxxxxxxxxxxxxxxx" // Redact the API key for logging.
slog.Debug("API key used for verification", "apiKey", redactedApiKey) //Use Debug level - avoid in production.
// Use Error Level not Info.
slog.Error("Failed API request", "status", 500, "endpoint", "/data", slog.String("error", "Timeout"))
}
"""
### 5.5. Anti-Patterns
* Printing stack traces directly to the user interface.
* Logging excessively, which can impact performance and storage costs.
* Not rotating log files, leading to disk space exhaustion.
* Skipping contextual information in log messages.
## 6. Data Protection at Rest
### 6.1. Standards
* **Do This:** Encrypt sensitive data at rest using standard encryption algorithms (AES, etc.).
* **Do This:** Manage encryption keys securely (e.g., using a Hardware Security Module - HSM, or key management service).
* **Don't Do This:** Store encryption keys alongside the data they protect.
### 6.2. Code Example - AES Encryption/Decryption (Simplified Example - Key Management Omitted for Brevity)
"""go
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"fmt"
"io"
"log"
)
func generateKey() []byte {
key := make([]byte, 32)
_, err := io.ReadFull(rand.Reader, key)
if err != nil {
log.Fatal(err)
}
return key
}
// encrypt encrypts data using AES-256 GCM.
func encrypt(key []byte, plaintext string) (string, error) {
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
aesGCM, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonce := make([]byte, aesGCM.NonceSize())
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
return "", err
}
ciphertext := aesGCM.Seal(nonce, nonce, []byte(plaintext), nil)
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
// decrypt decrypts data encrypted with AES-256 GCM.
func decrypt(key []byte, ciphertext string) (string, error) {
enc, err := base64.StdEncoding.DecodeString(ciphertext)
if err != nil {
return "", err
}
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
aesGCM, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonceSize := aesGCM.NonceSize()
nonce, ciphertextOnly := enc[:nonceSize], enc[nonceSize:]
plaintext, err := aesGCM.Open(nil, nonce, ciphertextOnly, nil)
if err != nil {
return "", err
}
return string(plaintext), nil
}
func main() {
key := generateKey() // In real implementation this is securely retrieved/managed
plaintext := "Sensitive data to protect"
ciphertext, err := encrypt(key, plaintext)
if err != nil {
log.Fatal(err)
}
fmt.Println("Encrypted:", ciphertext)
decryptedText, err := decrypt(key, ciphertext)
if err != nil {
log.Fatal(err)
}
fmt.Println("Decrypted:", decryptedText)
}
"""
## 7. Dependency Management
### 7.1. Standards
* Explicitly define the versions of all dependencies in "go.mod".
* Regularly audit dependencies for known vulnerabilities using tools like "govulncheck".
* Keep dependencies up to date; balance new features vs. potential breaking changes and regression introduction.
### 7.2. Code Example: Using "govulncheck"
"""bash
go install golang.org/x/vuln/cmd/govulncheck@latest # Install govulncheck
govulncheck ./... # Check your module
"""
## 8. Vulnerability Scanning in CI/CD Pipelines
* Integrate vulnerability scanning tools (e.g., "govulncheck", Snyk, SonarQube) into CI/CD pipelines to automatically detect and prevent the introduction of vulnerabilities.
This comprehensive document provides a strong foundation for developing secure Go applications. Remember to stay up-to-date with the latest security best practices and adapt these guidelines as needed based on the specific requirements of your projects.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# Deployment and DevOps Standards for Go This document outlines the deployment and DevOps standards for Go projects, guiding developers to build robust, scalable, and maintainable applications. It focuses on best practices for build processes, CI/CD pipelines, production considerations, and technology-specific details. ## 1. Build Processes and CI/CD ### 1.1. Standardizing Build Processes with Makefiles/Taskfiles * **Do This:** Use "make" or "taskfile" to define build processes. These tools allow for consistent, repeatable builds across different environments. They also provide a single point of entry for build-related commands, making it easier for developers to understand and contribute. * **Don't Do This:** Avoid ad-hoc build scripts or relying on IDE configurations. This creates inconsistencies and makes it harder to automate the build process. Don't hardcode environment-specific configurations directly into build commands; leverage environment variables. * **Why:** Centralized build definitions ensure consistency. Using "make" or taskfile" and similar tools reduces the risk of human error during build and deployment. Automating the build process enables Continuous Integration. * **Example (Makefile):** """makefile PROJECT_NAME := my-go-app VERSION := $(shell git describe --tags --always --dirty) BUILD_DIR := build .PHONY: all build clean run test all: build test build: @echo "Building $(PROJECT_NAME) version $(VERSION)..." go build -ldflags "-X main.version=$(VERSION)" -o $(BUILD_DIR)/$(PROJECT_NAME) ./cmd/main.go clean: @echo "Cleaning build directory..." rm -rf $(BUILD_DIR) run: build @echo "Running $(PROJECT_NAME)..." $(BUILD_DIR)/$(PROJECT_NAME) test: @echo "Running tests..." go test ./... docker: build docker build -t $(PROJECT_NAME):$(VERSION) . docker-push: docker docker push $(PROJECT_NAME):$(VERSION) """ This example demonstrates building a Go application, setting a version from git, cleaning the build directory, running tests, and building/pushing a Docker image. * **Example (Taskfile.yml):** """yaml version: "3" tasks: build: desc: Build the application cmds: - go build -o bin/myapp ./cmd/myapp test: desc: Run all tests cmds: - go test ./... run: desc: Run the application cmds: - go run ./cmd/myapp docker:build: desc: Build a Docker image cmds: - docker build -t myapp:latest . docker:push: desc: Push the Docker image cmds: - docker push myapp:latest """ ### 1.2. Versioning and Metadata * **Do This:** Embed version information in your Go binaries at build time using "-ldflags". Expose this information via a "/version" endpoint or CLI flag. Utilize semantic versioning consistently. * **Don't Do This:** Hardcode version strings or omit version information. This makes it difficult to track deployments and debug issues. * **Why:** Versioning is critical for identifying deployed builds, debugging, and rolling back deployments. Embedding versions at build time ensures the correct version is accessible within the application during runtime. * **Example:** """go package main import ( "fmt" "net/http" "os" ) var ( version = "dev" // Default, overwritten by ldflags ) func versionHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Version: %s\n", version) fmt.Fprintf(w, "Go Version: %s\n", runtime.Version()) fmt.Fprintf(w, "OS/Arch: %s %s\n", runtime.GOOS, runtime.GOARCH) } func main() { // Using environment variables for port configuration: port := os.Getenv("PORT") if port == "" { port = "8080" // Default if PORT is not set } http.HandleFunc("/version", versionHandler) fmt.Printf("Server listening on port %s...\n", port) http.ListenAndServe(":" + port, nil) } """ """bash go build -ldflags "-X main.version=$(git describe --tags --always --dirty)" -o myapp ./cmd/main.go """ This embeds the git version into the "version" variable. The example application serves this information via a web endpoint "/version". Runtime environment specifications are also output, and this relies on the "PORT" environmental variable. ### 1.3. Dependency Management * **Do This:** Use "go mod" for dependency management. Regularly update dependencies to patch security vulnerabilities and benefit from performance improvements. Use semantic versioning and pin your dependencies in "go.mod" to ensure reproducible builds. Consider a tool like Dependabot to automate dependency updates. * **Don't Do This:** Commit "vendor" directories to source control unless absolutely necessary (e.g., for reproducible builds in very specific, isolated environments). Don't ignore dependency updates. * **Why:** Modern Go projects use "go mod" for dependency management. It simplifies dependency tracking, versioning, and reproducible builds. * **Example ("go.mod"):** """ module my-go-app go 1.21 require ( github.com/gorilla/mux v1.8.0 github.com/stretchr/testify v1.8.4 ) """ ### 1.4. CI/CD Pipeline Configuration * **Do This:** Use a CI/CD system (e.g., Jenkins, GitLab CI, GitHub Actions, CircleCI) to automate builds, tests, and deployments. Define your pipeline as code (e.g., ".gitlab-ci.yml", ".github/workflows/main.yml"). Incorporate linting, security scanning, and integration tests into your pipeline. * **Don't Do This:** Manually deploy applications or rely on inconsistent CI/CD configurations. * **Why:** CI/CD pipelines automate the software delivery process, reducing errors and improving velocity. Pipeline-as-code ensures consistency and reproducibility of the pipeline. * **Example (GitHub Actions ".github/workflows/main.yml"):** """yaml name: CI/CD Pipeline on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Go uses: actions/setup-go@v4 with: go-version: '1.21' - name: Build run: go build -v ./... test: needs: build runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Go uses: actions/setup-go@v4 with: go-version: '1.21' - name: Test run: go test -v ./... lint: needs: build runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Go uses: actions/setup-go@v4 with: go-version: '1.21' - name: golangci-lint uses: golangci/golangci-lint-action@v3 with: version: latest args: --timeout=5m docker: needs: [test, lint] runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Build the Docker image run: docker build . -t my-go-app:latest - name: Login to Docker Hub (replace with your registry) run: docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} docker.io - name: Push the Docker image (replace with your registry) run: docker push my-go-app:latest """ This example defines stages for building, testing, linting, and building a Docker image. It uses secrets stored in GitHub for Docker Hub authentication. ### 1.5. Environment Configuration * **Do This:** Use environment variables for configuration. Consider tools like "viper" to read configuration from multiple formats (e.g., environment variables, files). Provide default values for optional configurations. * **Don't Do This:** Hardcode configuration values or store sensitive information in source code. * **Why:** Environment variables allow you to configure your application without modifying the code. This is essential for deploying applications in different environments (e.g., development, staging, production). * **Example:** """go package main import ( "fmt" "os" "github.com/spf13/viper" ) type Config struct { Port int "mapstructure:"port"" DatabaseURL string "mapstructure:"database_url"" AllowedOrigins []string "mapstructure:"allowed_origins"" } func LoadConfig(path string) (config Config, err error) { viper.AddConfigPath(path) viper.SetConfigName("app") viper.SetConfigType("env") // Or "json", "yaml", etc. if using files. viper.AutomaticEnv() err = viper.ReadInConfig() if err != nil { // If the config file is not found, it's okay to use environment variables. fmt.Println("Error reading config file, fallback to environment variables:", err) } // Set Defaults viper.SetDefault("port", 8080) viper.SetDefault("allowed_origins", []string{"http://localhost:3000"}) err = viper.Unmarshal(&config) return } func main() { config, err := LoadConfig(".") if err != nil { panic(err) } fmt.Printf("Port: %d\n", config.Port) fmt.Printf("Database URL: %s\n", config.DatabaseURL) fmt.Printf("Allowed Origins: %v\n", config.AllowedOrigins) // Using directly: dbURL := os.Getenv("DATABASE_URL") if dbURL == "" { fmt.Println("DATABASE_URL not set!") } } """ This example loads configuration from environment variables using viper. If a config file is present, "viper" will leverage that data first to override environmental variables. Defaults are set as well. It illustrates the hierarchy of config loading. It also shows how to query an environmental variable manually using "os.Getenv". ## 2. Production Considerations ### 2.1. Logging and Monitoring * **Do This:** Use structured logging (e.g., JSON) for easier parsing and analysis. Implement comprehensive monitoring using tools like Prometheus and Grafana. Include health check endpoints ("/healthz") for load balancers and orchestration systems. * Use context-aware logging to trace requests through your system. * Instrument your code with metrics to track performance and identify bottlenecks. * **Don't Do This:** Rely on "fmt.Println" for logging in production. Neglect monitoring. * **Why:** Effective logging and monitoring are essential for understanding application behavior, diagnosing issues, and optimizing performance in production. * **Example (Structured Logging with "zap"):** """go package main import ( "fmt" "net/http" "time" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) // Define a custom logger struct that wraps the zap logger type Logger struct { *zap.SugaredLogger } // InitializeLogger creates a new zap logger func InitializeLogger(logLevel string) (*Logger, error) { level := zapcore.InfoLevel switch logLevel { case "debug": level = zapcore.DebugLevel case "warn": level = zapcore.WarnLevel case "error": level = zapcore.ErrorLevel case "fatal": level = zapcore.FatalLevel } config := zap.Config{ Encoding: "json", // Output logs in JSON format Level: zap.NewAtomicLevelAt(level), // Set the minimum log level OutputPaths: []string{"stdout"}, // Log to standard output ErrorOutputPaths: []string{"stderr"}, // Log errors to standard error EncoderConfig: zapcore.EncoderConfig{ MessageKey: "message", LevelKey: "level", TimeKey: "time", NameKey: "name", CallerKey: "caller", StacktraceKey: "stacktrace", LineEnding: zapcore.DefaultLineEnding, EncodeLevel: zapcore.CapitalLevelEncoder, EncodeTime: zapcore.ISO8601TimeEncoder, EncodeDuration: zapcore.SecondsDurationEncoder, EncodeCaller: zapcore.ShortCallerEncoder, }, } logger, err := config.Build() if err != nil { return nil, fmt.Errorf("failed to initialize logger: %w", err) } sugar := logger.Sugar() return &Logger{sugar}, nil } func loggingMiddleware(logger *Logger, next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { start := time.Now() defer func() { duration := time.Since(start) logger.Infow("HTTP request", "method", r.Method, "path", r.URL.Path, "duration", duration.String(), "ip", r.RemoteAddr) }() next(w,r) } } func healthzHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "OK") } func main() { // Initialize logger logger, err := InitializeLogger("info") // or from env var if err != nil { panic(err) } defer logger.Sync() // Flush buffered logs before exiting // HTTP Server port := os.Getenv("PORT") if port == "" { port = "8080" } http.HandleFunc("/healthz", healthzHandler) // Expose a health check endpoint // Wrap handler func with the logging middleware http.HandleFunc("/", loggingMiddleware(logger, func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "Hello, World!") })) fmt.Printf("Server listening on port %s\n", port) if err := http.ListenAndServe(":" + port, nil); err != nil { logger.Fatalf("failed to start server: %v", err) } } """ This example uses "go.uber.org/zap" for structured logging. It configures a logger that outputs in JSON format and provides a middleware for logging HTTP requests, showcasing structured logging. It also demonstrates how to create a health check endpoint. The logger itself is configured with customizable levels, and its output location can be specified. * **Example (Prometheus Metrics with "prometheus/client_golang"):** """go package main import ( "fmt" "net/http" "time" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promhttp" ) var ( httpRequestsTotal = promauto.NewCounterVec(prometheus.CounterOpts{ Name: "http_requests_total", Help: "Total number of HTTP requests.", }, []string{"path"}) httpRequestDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ Name: "http_request_duration_seconds", Help: "Duration of HTTP requests.", }, []string{"path"}) ) func instrumentHandler(path string, handler http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { start := time.Now() handler(w, r) duration := time.Since(start) httpRequestsTotal.With(prometheus.Labels{"path": path}).Inc() httpRequestDuration.With(prometheus.Labels{"path": path}).Observe(duration.Seconds()) } } func healthzHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "OK") } func main() { http.HandleFunc("/healthz", healthzHandler) http.Handle("/metrics", promhttp.Handler()) http.HandleFunc("/", instrumentHandler("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "Hello, World!") })) fmt.Println("Server listening on port 8080") http.ListenAndServe(":8080", nil) } """ This example provides HTTP metrics to Prometheus. Includes counters and histograms and also exposes a "/metrics" endpoint. The "instrumentHandler" function wraps the root handler and captures key metrics about the response for further use in Prometheus and Grafana. ### 2.2. Error Handling * **Do This:** Implement robust error handling. Use custom error types. Handle panics gracefully by logging the error and recovering (if appropriate). Utilize "errors.Is" and "errors.As" for error checking instead of direct string comparison. * **Don't Do This:** Ignore errors or propagate errors without context. Avoid naked panics. * **Why:** Robust error handling prevents cascading failures and provides valuable debugging information. Custom error types allow you to handle different error scenarios appropriately. * **Example:** """go package main import ( "errors" "fmt" "log" ) type DatabaseError struct { Message string Code int } func (e *DatabaseError) Error() string { return fmt.Sprintf("Database Error: %s (Code: %d)", e.Message, e.Code) } func fetchData() (string, error) { // Simulate a database error. return "", &DatabaseError{Message: "Connection failed", Code: 500} } func processData() (string, error) { data, err := fetchData() if err != nil { // Wrap the original error with context. return "", fmt.Errorf("failed to fetch data: %w", err) } // Process the data... return data, nil } func main() { // Using an anonymous function to handle the panic defer func() { if r := recover(); r != nil { // Log the panic error and stack trace log.Printf("Recovered from panic: %v", r) // Optionally, perform cleanup or shutdown tasks here } }() result, err := processData() if err != nil { if errors.Is(err, &DatabaseError{}) { var dbErr *DatabaseError if errors.As(err, &dbErr) { fmt.Printf("Database error occurred: %s, Code: %d\n", dbErr.Message, dbErr.Code) } } else { fmt.Printf("An unexpected error occurred: %v\n", err) } return } fmt.Println("Data:", result) } """ This example defines a custom error type "DatabaseError" and uses "errors.Is" and "errors.As" for error checking. It wraps errors with context and handle panics using "recover" in a deferred function. ### 2.3. Concurrency and Goroutine Management * **Do This:** Use goroutines and channels effectively. Limit the number of concurrent goroutines to avoid resource exhaustion. Use errgroup manage and synchronize related goroutines. Use context to manage goroutine lifetimes. * **Don't Do This:** Launch an unbounded number of goroutines or leak goroutines. * **Why:** Go's concurrency model (goroutines and channels) is powerful but requires careful management to prevent resource exhaustion and deadlocks. * **Example (Using "errgroup"):** """go package main import ( "context" "fmt" "net/http" "time" "golang.org/x/sync/errgroup" ) func fetchURL(ctx context.Context, url string) error { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return err } resp, err := http.DefaultClient.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode >= 400 { return fmt.Errorf("request failed with status code: %d", resp.StatusCode) } fmt.Printf("Successfully fetched %s\n", url) return nil } func main() { const requestTimeout = 5 * time.Second urls := []string{ "https://www.google.com", "https://www.example.com", "https://www.golang.org", } ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) // Timeout after 5 seconds. defer cancel() var eg errgroup.Group for _, url := range urls { url := url // Capture the loop variable. Prevents data races. eg.Go(func() error { return fetchURL(ctx, url) }) } if err := eg.Wait(); err != nil { fmt.Printf("An error occurred: %v\n", err) } else { fmt.Println("All URLs fetched successfully.") } } """ This shows how to use an "errgroup" to manage a pool of goroutines and cancel all tasks if one fails. Context is used to limit the overall execution time. ### 2.4. Security * **Do This:** Regularly scan your code and dependencies for security vulnerabilities. Follow security best practices (e.g., input validation, output encoding, authentication, authorization). Use HTTPS for all network communication. Avoid storing sensitive data in plaintext. Fuzz test your code. * **Don't Do This:** Ignore security vulnerabilities or rely on weak security measures. * **Why:** Security vulnerabilities can lead to data breaches, service disruptions, and other serious consequences. Proactive security measures are essential for protecting your applications and data. * **Specific Go Security Considerations:** * Be aware of the potential for injection vulnerabilities (e.g., SQL injection, command injection) when constructing dynamic queries or commands. * Use the "crypto" package carefully and avoid implementing custom encryption algorithms unless you are a cryptography expert. * Protect against cross-site scripting (XSS) vulnerabilities by encoding output appropriately when generating HTML. * Sanitize user input. ### 2.5. Graceful Shutdown * **Do This:** Implement graceful shutdown to allow your application to finish processing in-flight requests before exiting. Use "context.WithCancel" and a "signal.Notify" channel to handle shutdown signals (e.g., "SIGINT", "SIGTERM"). * **Don't Do This:** Terminate your application abruptly, potentially losing data or leaving clients in an inconsistent state. * **Why:** Graceful shutdown ensures that your application can exit cleanly without disrupting users or losing data. This allows services to be restarted with zero downtime. * **Example:** """go package main import ( "context" "fmt" "net/http" "os" "os/signal" "syscall" "time" ) func main() { // Create a channel to listen for interrupt signals. sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) // Create a context that can be cancelled. ctx, cancel := context.WithCancel(context.Background()) // Create a HTTP server. server := &http.Server{ Addr: ":8080", Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "Hello, world!") }), } // Start the server in a goroutine. go func() { fmt.Println("Server listening on :8080") if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { fmt.Printf("Server error: %v\n", err) cancel() // Cancel context on server error. } }() // Goroutine to simulate some background work go func() { ticker := time.NewTicker(2 * time.Second) defer ticker.Stop() for { select { case <-ticker.C: fmt.Println("Performing background task...") case <-ctx.Done(): fmt.Println("Stopping background task due to shutdown...") return } } }() // Wait for a shutdown signal or context cancellation. <-sigChan fmt.Println("Shutdown signal received.") cancel() // Cancel the context. // Give the server a maximum of 5 seconds to shutdown. shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second) defer shutdownCancel() // Shutdown the server gracefully. if err := server.Shutdown(shutdownCtx); err != nil { fmt.Printf("Server shutdown error: %v\n", err) } fmt.Println("Server shutdown complete.") } """ This example shows how to use "signal.Notify" to listen for shutdown signals and "context.WithTimeout" to implement a timeout for the shutdown process. By adhering to these standards, development teams can ensure their Go applications are built robustly, deployed efficiently, and operate reliably in production environments, while also improving security posture. Modern tooling and language features provided by Go make these standards easier to implement and maintain.
# Performance Optimization Standards for Go This document outlines the performance optimization standards for Go, focusing on techniques to improve application speed, responsiveness, and resource usage. It provides specific, actionable guidelines, along with explanations, code examples, and common anti-patterns. ## I. Architectural Considerations ### 1. Select Appropriate Data Structures and Algorithms Choosing the right data structure and algorithm is crucial for performance. Go's standard library provides a range of options, each with different performance characteristics. **Do This:** * Understand the time and space complexity of different data structures. Use benchmarks to validate assumptions. * Choose data structures that best fit the specific use case (e.g., maps for key-value lookups, slices for ordered data). * Favor algorithms with lower complexity for frequently executed operations. **Don't Do This:** * Use inefficient algorithms without considering alternatives (e.g., linear search when a binary search is possible). * Prematurely optimize algorithms without profiling. **Why:** Proper data structure and algorithm selection can significantly reduce the execution time and memory footprint of your code. **Example:** """go package main import ( "fmt" "time" ) func linearSearch(arr []int, target int) bool { for _, val := range arr { if val == target { return true } } return false } func binarySearch(arr []int, target int) bool { low := 0 high := len(arr) - 1 for low <= high { mid := (low + high) / 2 if arr[mid] < target { low = mid + 1 } else if arr[mid] > target { high = mid - 1 } else { return true } } return false } func main() { arr := []int{2, 3, 4, 10, 40} target := 10 start := time.Now() linearSearchResult := linearSearch(arr, target) linearSearchTime := time.Since(start) start = time.Now() binarySearchResult := binarySearch(arr, target) binarySearchTime := time.Since(start) fmt.Println("Linear Search Result:", linearSearchResult, "Time:", linearSearchTime) fmt.Println("Binary Search Result:", binarySearchResult, "Time:", binarySearchTime) } """ ### 2. Concurrency and Parallelism Go excels at concurrency and parallelism. Properly leveraging goroutines and channels can dramatically improve performance. **Do This:** * Use goroutines for concurrent execution of independent tasks. * Use channels for safe communication and synchronization between goroutines. * Consider the "sync" package for more advanced synchronization primitives. * Leverage "go" build tags to specialize code and dependencies for optimized architectures. **Don't Do This:** * Create excessive goroutines, which can lead to context-switching overhead. Use worker pools to limit concurrency. * Use shared memory without proper synchronization, leading to race conditions. * Create goroutine leaks resulting in performance degradation as resources are consumed without release. **Why:** Concurrency allows your program to execute multiple tasks simultaneously, reducing overall execution time. Parallelism spreads tasks across multiple CPU cores, maximizing hardware utilization. **Example (Worker Pool):** """go package main import ( "fmt" "math/rand" "sync" "time" ) type Job struct { ID int Data int } type Result struct { JobID int Value int } func worker(jobs <-chan Job, results chan<- Result, wg *sync.WaitGroup) { defer wg.Done() for job := range jobs { // Simulate some work time.Sleep(time.Duration(rand.Intn(500)) * time.Millisecond) result := job.Data * 2 results <- Result{JobID: job.ID, Value: result} } } func main() { numJobs := 100 numWorkers := 10 jobs := make(chan Job, numJobs) results := make(chan Result, numJobs) var wg sync.WaitGroup for i := 0; i < numWorkers; i++ { wg.Add(1) go worker(jobs, results, &wg) } go func(){ wg.Wait() close(results) }() // Send jobs for i := 0; i < numJobs; i++ { jobs <- Job{ID: i, Data: i} } close(jobs) // Collect results for result := range results { fmt.Printf("Job ID: %d, Result: %d\n", result.JobID, result.Value) } } """ ## II. Code-Level Optimization ### 1. Minimize Memory Allocations Frequent memory allocations can be a major performance bottleneck. **Do This:** * Use "sync.Pool" to reuse frequently allocated objects. * Pre-allocate slices and maps with known sizes, minimizing reallocations. * Use "bytes.Buffer" or "strings.Builder" for efficient string concatenation. * Profile memory allocation using "go tool pprof". **Don't Do This:** * Allocate memory inside tight loops without considering alternatives. * Ignore memory leaks. Use tools to detect and fix them. * Rely on short-lived variables for quick code, which may be eligible for escape analysis and heap allocation. **Why:** Reducing memory allocations reduces the load on the garbage collector, improving application responsiveness. **Example (using "sync.Pool"):** """go package main import ( "fmt" "sync" ) type MyObject struct { Data string } var objectPool = sync.Pool{ New: func() interface{} { return &MyObject{} }, } func main() { // Get an object from the pool obj := objectPool.Get().(*MyObject) obj.Data = "Hello, Pool!" fmt.Println(obj.Data) // Release the object back to the pool objectPool.Put(obj) // Get another object -- potentially the same one! obj2 := objectPool.Get().(*MyObject) fmt.Println(obj2.Data) // Possibly "Hello, Pool!", but could be an empty string too. objectPool.Put(obj2) } """ ### 2. String Manipulation String operations can be expensive. Use appropriate techniques for string manipulation. **Do This:** * Use "strings.Builder" for building strings dynamically. * Avoid unnecessary string conversions. * Consider "[]byte" operations for performance-critical tasks. **Don't Do This:** * Use "+=" for repeated string concatenation in loops, leading to repeated allocations. * Convert "string" to "[]rune" unnecessarily. **Why:** Efficient string manipulation reduces memory allocations and avoids redundant copying of data. **Example (strings.Builder):** """go package main import ( "fmt" "strings" ) func main() { var builder strings.Builder for i := 0; i < 1000; i++ { builder.WriteString(fmt.Sprintf("%d ", i)) } result := builder.String() fmt.Println(result) } """ ### 3. Avoid Reflection Reflection can be powerful, but it's often slower than direct method calls. **Do This:** * Use interfaces to achieve polymorphism without reflection. * Consider code generation to avoid runtime reflection. **Don't Do This:** * Overuse reflection in performance-critical sections of code. **Why:** Reflection involves runtime type checking and dynamic method dispatch, which can introduce overhead. ### 4. Inlining Go's compiler attempts to inline small functions to avoid the overhead of function calls. **Do This:** * Keep frequently called functions small to encourage inlining. * Use build flags to optimize code for specific architectures ("-gcflags="-l"" to disable inlining for debugging). **Don't Do This:** * Write excessively large functions that the compiler is less likely to inline. **Why:** Inlining reduces function call overhead, such as pushing parameters onto the stack and jumping to the function's address. ### 5. Effective use of "defer" While "defer" simplifies resource management, it comes with a performance cost. **Do This:** * Use "defer" for critical cleanup tasks such as closing files or releasing locks. * Consider upfront cleanup for performance-critical cases particularly in tight loops. **Don't Do This:** * Use "defer" excessively, which can add overhead to function execution. **Why:** Defer statements execute at the end of the function, introducing a slight overhead compared to manual cleanup. **Example:** """go package main import ( "fmt" "os" ) func readFile(filename string) error { file, err := os.Open(filename) if err != nil { return err } //Good - this ensures the file is closed even if errors occur defer file.Close() // Ensure file is closed // Read from file (implementation omitted) fmt.Println("Reading file.") return nil } func main() { err := readFile("example.txt") if err != nil { fmt.Println("Error:", err) } } """ ## III. Network and I/O Optimization ### 1. Connection Pooling Reusing network connections can significantly reduce latency. **Do This:** * Use connection pooling libraries like "net/http"'s "Transport" to manage HTTP connections. * Configure connection pool parameters (e.g., maximum idle connections) appropriately. **Don't Do This:** * Create new connections for every request, which adds overhead. **Why:** Establishing new connections involves network handshakes and resource allocation, which can be time-consuming. **Example (HTTP Connection Pooling):** """go package main import ( "fmt" "io" "net/http" "time" ) func main() { transport := &http.Transport{ MaxIdleConns: 10, IdleConnTimeout: 30 * time.Second, DisableCompression: true, } client := &http.Client{Transport: transport} for i := 0; i < 10; i++ { resp, err := client.Get("https://example.com") if err != nil { fmt.Println("Error:", err) continue } io.Copy(io.Discard, resp.Body) resp.Body.Close() fmt.Println("Request", i, "completed.") } } """ ### 2. Buffering Buffering can improve I/O performance. **Do This:** * Use buffered readers and writers (e.g., "bufio.Reader", "bufio.Writer") for file and network I/O. * Choose appropriate buffer sizes. **Don't Do This:** * Perform unbuffered I/O, especially for small reads and writes. **Why:** Buffering reduces the number of system calls, improving I/O throughput. **Example (Buffered Writer):** """go package main import ( "bufio" "fmt" "os" ) func main() { file, err := os.Create("output.txt") if err != nil { fmt.Println("Error:", err) return } defer file.Close() bufferedWriter := bufio.NewWriter(file) for i := 0; i < 1000; i++ { _, err := bufferedWriter.WriteString(fmt.Sprintf("Line %d\n", i)) if err != nil { fmt.Println("Error:", err) break } } err = bufferedWriter.Flush() // Flush the buffer to disk if err != nil { fmt.Println("Error:", err) } fmt.Println("File written.") } """ ### 3. gRPC and Protocol Buffers If applicable, gRPC and Protocol Buffers can notably enhance the performance of network-based applications. **Do This:** * Use gRPC and Protocol Buffers for high-performance RPC services. * Optimize Protocol Buffer schemas for size and performance. **Don't Do This:** * Use inefficient serialization formats like JSON for large data transfers. **Why:** Protocol Buffers are a compact and efficient binary serialization format, and gRPC provides a high-performance RPC framework. ## IV. Profiling and Benchmarking ### 1. Use "go tool pprof" Profiling is essential for identifying performance bottlenecks. **Do This:** * Use "go tool pprof" to profile CPU, memory, and blocking operations. * Analyze the profile data to identify hotspots in your code. * Set reasonable profiling sampling rates. **Don't Do This:** * Guess at performance bottlenecks without profiling. * Ignore the profile data, which can provide valuable insight into performance. **Why:** Profiling provides concrete data to guide optimization efforts. **Example (Profiling):** """go package main import ( "fmt" "log" "os" "runtime" "runtime/pprof" "time" ) func main() { // Create a CPU profile file cpuProfile, err := os.Create("cpu.prof") if err != nil { log.Fatal("could not create CPU profile: ", err) } defer cpuProfile.Close() if err := pprof.StartCPUProfile(cpuProfile); err != nil { log.Fatal("could not start CPU profile: ", err) } defer pprof.StopCPUProfile() // Simulate some CPU-intensive work for i := 0; i < 10000000; i++ { _ = i * i } // Create a memory profile file memProfile, err := os.Create("mem.prof") if err != nil { log.Fatal("could not create memory profile: ", err) } defer memProfile.Close() runtime.GC() // Get up-to-date allocation stats if err := pprof.WriteHeapProfile(memProfile); err != nil { log.Fatal("could not write memory profile: ", err) } fmt.Println("Profiling completed. Run "go tool pprof cpu.prof" and "go tool pprof mem.prof" to analyze the results.") } """ ### 2. Write Benchmarks Benchmarks allow you to measure the performance of specific code snippets. **Do This:** * Use the "testing" package to write benchmarks. * Use "go test -bench=." to run the benchmarks. * Use the "-benchmem" flag to measure memory allocations. * Use table-driven benchmarks to test multiple scenarios. **Don't Do This:** * Rely on intuition without benchmarking. * Ignore memory allocations in benchmarks. **Why:** Benchmarks provide quantitative data to compare different implementations and ensure that optimizations are effective. **Example (Benchmark):** """go package main import ( "strings" "testing" ) func BenchmarkStringBuilder(b *testing.B) { for i := 0; i < b.N; i++ { var builder strings.Builder for j := 0; j < 100; j++ { builder.WriteString("hello") } _ = builder.String() } } func BenchmarkStringConcat(b *testing.B) { for i := 0; i < b.N; i++ { s := "" for j := 0; j < 100; j++ { s += "hello" } _ = s } } //Run 'go test -bench=.' to execute benchmarks """ ## V. Updates Based on Recent Go Releases/Features Go evolves, integrating improvements and newly recommended methods. Staying current with advice from recent release notes and Go team blog posts is critical. ### 1. Compiler Optimizations * **Do This:** Stay aware of compiler improvements in each Go release. Newer compilers often generate more efficient code automatically, so upgrading regularly can yield noticeable performance gains without code modifications. Review release notes regarding flags like "-gcflags" that can influence compiler behavior, and experiment with these in benchmarks. * **Don't Do This:** Assume older compiler optimizations are sufficient. Regularly updating ensures access to the latest enhancements. ### 2. Arena Allocation (Experimental as of Go 1.21, potential for future standard library integration) * **Do This:** Investigate experimental arena allocation packages for use cases with many short-lived allocations of the same type. If standard library support arrives, adopt this pattern where it fits. This minimizes GC overhead. * **Don't Do This:** Blindly integrate arenas without thorough profiling to confirm performance benefits. Arenas add complexity and might not be faster in all scenarios if object lifetimes become complex. ### 3. Improved Standard Library Algorithms * **Do This:** Check release notes for optimizations to existing functions and data structures in the standard library. For example, newer versions of "sort.Slice" might use more advanced algorithms internally, leading to performance increases. * **Don't Do This:** Re-implement standard library algorithms, unless you have highly specialized needs and profiling conclusively demonstrates a significant improvement over the standard version. ### 4. "unsafe" Package Considerations (Proceed with extreme caution) * **Do This:** Understand that leveraging the "unsafe" package can provide performance gains in highly specific situations, such as directly manipulating memory layouts or circumventing type safety checks. Use sparingly and only when absolutely necessary. Document clearly the reason for its use, and include extensive testing. * **Don't Do This:** Use "unsafe" to simply "optimize" code without a firm understanding of its implications. Misuse of "unsafe" can cause memory corruption, crashes, and security vulnerabilities. Prefer safe, well-tested alternatives whenever possible. Be prepared to revisit "unsafe" usage with each Go release, as internal data structure layouts may change. ## VI. Security Implications of Performance Optimizations Certain performance optimizations can unintentionally introduce or exacerbate security vulnerabilities. **Do This:** * **Bounds Checking:** Be aware that disabling bounds checking (e.g., using "unsafe" or compiler flags) can lead to buffer overflows if not handled with extreme care. Ensure thorough validation of array/slice indices. * **Data Races:** When optimizing concurrent code, rigorously test for data races. Tools like "-race" should be part of your testing suite. Address race conditions promptly to prevent unpredictable behavior and potential security exploits. * **Side-Channel Attacks:** With cryptography or sensitive data processing, be mindful of side-channel attacks (e.g., timing attacks). Avoid optimizations that introduce timing variations dependent on secret data. Use constant-time algorithms and operations whenever possible. * **Input Validation:** Never skip input validation in the name of performance as this can lead to vulnerabilities. **Don't Do This:** * Sacrifice security for marginal performance gains. * Assume optimizations are safe without careful analysis and testing. ## VII. Conclusion This document provides a comprehensive set of guidelines for optimizing Go code for performance. By following these standards, developers can write more efficient, responsive, and maintainable applications. Regular profiling and benchmarking are crucial to identify bottlenecks and ensure that optimizations are effective. Stay current with new releases and incorporate the latest best practices for maximum results.
# Core Architecture Standards for Go This document outlines the core architectural standards for Go projects, providing guidance on fundamental patterns, project structure, and organization principles. These standards aim to ensure maintainability, performance, and security. ## 1. Architectural Patterns Choosing the right architectural pattern is crucial for long-term project success. Here are some commonly used patterns in Go and guidelines for their application. ### 1.1 Microservices **Standard:** When building complex, scalable applications, consider using a microservices architecture. * **Do This:** Design independent, deployable services with well-defined APIs. * **Don't Do This:** Build monolithic applications that are difficult to scale and maintain. **Why:** Microservices promote modularity, independent scaling, and technology diversity. **Code Example:** """go // Service A definition package main import ( "fmt" "net/http" "log" "os" ) func healthHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Service A is healthy") } func main() { port := os.Getenv("PORT") if port == "" { port = "8080" // Default port if not specified } http.HandleFunc("/health", healthHandler) log.Printf("Service A listening on port %s", port) log.Fatal(http.ListenAndServe(":"+port, nil)) } """ """go // Service B definition package main import ( "fmt" "net/http" "log" "os" ) func healthHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Service B is healthy") } func main() { port := os.Getenv("PORT") if port == "" { port = "8081" // Default port if not specified } http.HandleFunc("/health", healthHandler) log.Printf("Service B listening on port %s", port) log.Fatal(http.ListenAndServe(":"+port, nil)) } """ **Anti-pattern:** Tightly coupled services that depend on each other's internal implementation details. ### 1.2 Clean Architecture **Standard:** Structure the application with independent layers (presentation, application, domain, infrastructure). * **Do This:** Implement separation of concerns, with the domain layer containing business logic and the infrastructure layer handling external dependencies. * **Don't Do This:** Mix business logic with framework-specific code. **Why:** Clean Architecture promotes testability, maintainability, and adaptability to changing technologies. **Code Example:** """go // domain/user.go package domain type User struct { ID int Name string Email string } type UserRepository interface { Get(id int) (*User, error) Save(user *User) error } """ """go // application/user_service.go package application import "example.com/your_project/domain" type UserService struct { userRepo domain.UserRepository } func NewUserService(userRepo domain.UserRepository) *UserService { return &UserService{userRepo: userRepo} } func (s *UserService) GetUser(id int) (*domain.User, error) { return s.userRepo.Get(id) } """ """go // infrastructure/user_repository.go package infrastructure import "example.com/your_project/domain" type UserRepositoryImpl struct { // Database connection or other dependencies } func NewUserRepositoryImpl() *UserRepositoryImpl { return &UserRepositoryImpl{} } func (r *UserRepositoryImpl) Get(id int) (*domain.User, error) { // Implementation to fetch user from database return &domain.User{ID: id, Name: "Example User", Email: "example@example.com"}, nil // Mock for example } func (r *UserRepositoryImpl) Save(user *domain.User) error { // Implementation to save user to database return nil // Mock for example } """ **Anti-pattern:** God classes or functions that handle multiple responsibilities. ### 1.3 Hexagonal Architecture (Ports and Adapters) **Standard:** Isolate the application core from external components using ports and adapters. * **Do This:** Define interfaces (ports) for interacting with external systems and implement adapters for specific technologies. * **Don't Do This:** Directly couple the application core to external dependencies. **Why:** Hexagonal Architecture facilitates swapping out external systems without modifying the core logic. **Code Example:** """go // application/port/email_service.go package port // EmailService defines the interface for sending emails. type EmailService interface { SendEmail(to, subject, body string) error } """ """go // infrastructure/adapter/smtp_email_service.go package adapter import ( "net/smtp" "example.com/your_project/application/port" ) type SMTPEmailService struct { // SMTP configuration Host string Port int Username string Password string } func NewSMTPEmailService(host string, port int, username, password string) port.EmailService { return &SMTPEmailService{ Host: host, Port: port, Username: username, Password: password, } } // SendEmail sends an email via SMTP. func (s *SMTPEmailService) SendEmail(to, subject, body string) error { auth := smtp.PlainAuth("", s.Username, s.Password, s.Host) msg := []byte("To: " + to + "\r\n" + "Subject: " + subject + "\r\n" + "\r\n" + body + "\r\n") err := smtp.SendMail(s.Host+":"+string(s.Port), auth, s.Username, []string{to}, msg) if err != nil { return err } return nil } """ """go // application/service.go package application import "example.com/your_project/application/port" type AppService struct { emailService port.EmailService } // NewAppService creates a new AppService with the given EmailService. func NewAppService(emailService port.EmailService) *AppService { return &AppService{emailService: emailService} } // SendWelcomeEmail sends a welcome email to the given email address. func (s *AppService) SendWelcomeEmail(email string) error { subject := "Welcome!" body := "Welcome to our service!" return s.emailService.SendEmail(email, subject, body) } """ **Anti-pattern:** Hardcoding dependencies to specific implementations within the application core. ## 2. Project Structure and Organization A well-organized project structure enhances readability, maintainability, and collaboration. ### 2.1 Standard Layout **Standard:** Use a standard project layout to ensure consistency across projects. * **Do This:** Adopt the [standard layout](https://github.com/golang-standards/project-layout) as a template. * **Don't Do This:** Create ad-hoc project structures without clear guidelines. **Why:** A standard layout facilitates finding code, understanding dependencies, and contributing to the project. Example directory structure: """ ├── cmd │ └── your_app │ └── main.go # Application entry point ├── internal │ ├── app # Application logic │ ├── domain # Business domain │ └── infrastructure # Infrastructure code (DB, networking, etc.) ├── pkg # Reusable packages ├── api # API definitions (protobuf, gRPC) ├── scripts # Deployment and build scripts ├── vendor # Dependencies managed by Go modules ├── go.mod └── go.sum """ ### 2.2 Package Organization **Standard:** Organize code into packages based on functionality and responsibility. * **Do This:** Group related types and functions into a single package. Keep package names short, meaningful, and avoid using "util" or "helper" alone. * **Don't Do This:** Create large, monolithic packages with unrelated code. **Why:** Packages promote modularity, code reuse, and encapsulation. **Code Example:** """go // package user // User related logic package user type User struct { ID int Name string Email string } func NewUser(name, email string) *User { return &User{Name: name, Email: email} } // Validate validates the user data. func (u *User) Validate() error { if u.Name == "" { return fmt.Errorf("name cannot be empty") } if u.Email == "" { return fmt.Errorf("email cannot be empty") } // Basic email validation (you can add more sophisticated validation) if !strings.Contains(u.Email, "@") { return fmt.Errorf("invalid email format") } return nil } """ **Anti-pattern:** Packages with unclear responsibilities or containing code that belongs to other packages. ### 2.3 Dependency Management **Standard:** Use Go modules for managing dependencies. Avoid "vendor" directories unless absolutely necessary. * **Do This:** Use "go mod init" to initialize a new module and "go get" to add dependencies. To update dependencies, use "go get -u all". * **Don't Do This:** Manually manage dependencies or rely on outdated tools. **Why:** Go modules provide versioning, reproducibility, and security. **Code Example:** """shell go mod init your_project go get github.com/gorilla/mux go get -u all go mod tidy //Removes unused dependencies """ **Anti-pattern:** Checking in the "vendor" directory into version control unless necessary (for specific build environments). ## 3. Error Handling Proper error handling is crucial for building robust applications. ### 3.1 Explicit Error Handling **Standard:** Always check errors returned by functions. * **Do This:** Use the "if err != nil" pattern to handle errors explicitly. * **Don't Do This:** Ignore errors or use the blank identifier ("_") to discard them. **Why:** Explicit error handling prevents unexpected behavior and facilitates debugging. **Code Example:** """go package main import ( "fmt" "os" ) func readFile(filename string) (string, error) { content, err := os.ReadFile(filename) if err != nil { return "", fmt.Errorf("failed to read file: %w", err) } return string(content), nil } func main() { content, err := readFile("myfile.txt") if err != nil { fmt.Println("Error:", err) os.Exit(1) } fmt.Println("File content:", content) } """ **Anti-pattern:** Assuming functions always succeed without checking return values. ### 3.2 Error Wrapping Standard: When returning errors, wrap them to provide context. * Do This: Use "fmt.Errorf" with the "%w" verb to wrap errors. * Don't Do This: Return bare errors without context. Why: Error wrapping provides a stack trace and facilitates debugging. Code Example: (See example above) ### 3.3 Custom Error Types **Standard:** Define custom error types when specific error handling is required. * **Do This:** Create error types that implement the "error" interface and provide additional information. * **Don't Do This:** Rely solely on generic error types when specific properties need to be checked. **Why:** Custom error types enable more granular error handling and provide better information about the error. **Code Example:** """go package main import ( "fmt" "errors" ) // Define a custom error type type ValidationError struct { Field string Message string } // Implement the error interface for ValidationError func (e *ValidationError) Error() string { return fmt.Sprintf("validation error on field %s: %s", e.Field, e.Message) } // Function that returns the custom error func validateInput(input string) error { if input == "" { return &ValidationError{ Field: "input", Message: "input cannot be empty", } } return nil } func main() { err := validateInput("") if err != nil { // Check if the error is a ValidationError var validationErr *ValidationError if errors.As(err, &validationErr) { fmt.Printf("Validation Error: Field=%s, Message=%s\n", validationErr.Field, validationErr.Message) } else { fmt.Println("Other Error:", err) } } else { fmt.Println("Input is valid") } } """ **Anti-pattern:** Overusing custom error types when basic error wrapping suffices. ## 4. Concurrency Go's concurrency features are powerful, but require careful handling. ### 4.1 Goroutines **Standard:** Use goroutines for concurrent execution of tasks. * **Do This:** Launch goroutines using the "go" keyword. * **Don't Do This:** Create excessive goroutines without managing their lifecycle. **Why:** Goroutines enable efficient concurrency without the overhead of threads. **Code Example:** """go package main import ( "fmt" "time" ) func worker(id int, jobs <-chan int, results chan<- int) { for j := range jobs { fmt.Printf("worker %d, processing job %d\n", id, j) time.Sleep(time.Second) // Simulate work results <- j * 2 } } func main() { const numJobs = 5 jobs := make(chan int, numJobs) results := make(chan int, numJobs) // Start 3 workers for i := 1; i <= 3; i++ { go worker(i, jobs, results) } // Send jobs for i := 1; i <= numJobs; i++ { jobs <- i } close(jobs) // Collect results for i := 1; i <= numJobs; i++ { result := <-results fmt.Println("Result:", result) } close(results) fmt.Println("Done") } """ **Anti-pattern:** Launching goroutines without proper synchronization or error handling. ### 4.2 Channels **Standard:** Use channels for communication and synchronization between goroutines. * **Do This:** Send and receive data through channels to coordinate concurrent tasks. * **Don't Do This:** Use shared memory and locks directly unless absolutely necessary. **Why:** Channels provide a safe and efficient way to exchange data between concurrent goroutines. **Code Example:** (See example above) ### 4.3 Context **Standard:** Use "context.Context" to manage the lifecycle of goroutines and propagate cancellation signals. * **Do This:** Pass a "context.Context" to all functions that may be long-running or need to be cancelled. * **Don't Do This:** Ignore context or create custom context implementations without a clear justification. **Why:** Context enables graceful shutdown and cancellation of running goroutines. **Code Example:** """go package main import ( "context" "fmt" "time" ) func doWork(ctx context.Context, id int) { for i := 0; i < 5; i++ { select { case <-ctx.Done(): fmt.Printf("Worker %d: cancelled\n", id) return default: fmt.Printf("Worker %d: working on step %d\n", id, i) time.Sleep(time.Second) } } fmt.Printf("Worker %d: finished\n", id) } func main() { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() go doWork(ctx, 1) <-ctx.Done() // Wait for context to be cancelled or timeout fmt.Println("Program finished") } """ **Anti-pattern:** Ignoring context values or not propagating cancellation signals. ## 5. Logging Effective logging is critical for debugging and monitoring applications. ### 5.1 Structured Logging **Standard:** Use structured logging to generate machine-readable log entries. * **Do This:** Use libraries like "zap" or "logrus" to log data in a structured format (e.g., JSON). * **Don't Do This:** Use plain text logging, which is difficult to parse and analyze. **Why:** Structured logging facilitates analysis and monitoring of application behavior. **Code Example:** """go package main import ( "time" "go.uber.org/zap" ) func main() { logger, _ := zap.NewProduction() defer logger.Sync() // flushes buffer, if any sugar := logger.Sugar() sugar.Infow("Failed to fetch URL.", "url", "http://example.com", "attempt", 3, "backoff", time.Second) sugar.Infof("Failed to fetch URL: %s", "http://example.com") //Level Logger logger.Info("info log", zap.String("component", "MyApp"), zap.Int("id", 123)) } """ **Anti-pattern:** Logging sensitive information (e.g., passwords, API keys) to log files. Use proper redaction techniques. ### 5.2 Log Levels **Standard:** Use appropriate log levels (debug, info, warn, error, fatal) to indicate the severity of log messages. * **Do This:** Use "debug" for detailed debugging information, "info" for general application events, "warn" for potential issues, "error" for errors that require attention, and "fatal" for critical errors that cause the application to exit. * **Don't Do This:** Use incorrect log levels, which can obscure important information. **Why:** Log levels help filter and prioritize log messages. **Code Example:** (See example above) ### 5.3 Contextual Logging **Standard:** Include relevant context information in log messages (e.g., request ID, user ID). * **Do This:** Add fields to log entries to provide additional context. * **Don't Do This:** Log messages without sufficient context. **Why:** Contextual logging makes it easier to correlate log messages and troubleshoot issues. **Code Example:** (See Example above - adding component and id as "zap.String" and "zap.Int") ## Conclusion These architectural standards provide a solid foundation for building robust, maintainable, and scalable Go applications. By adhering to these standards, developers can create high-quality code that is easier to understand, test, and evolve over time, supporting long-term project success and maintainability.
# State Management Standards for Go This document outlines the best practices for managing application state, data flow, and reactivity in Go applications. It provides actionable standards, code examples, and explanations to guide developers in building maintainable, performant, and secure Go applications. These standards focus on modern approaches and patterns based on best practices and the latest Go features. ## 1. General Principles ### 1.1. Explicit State Management * **Do This:** Favor explicit state management techniques, where the flow of state is clearly defined and easily traceable. * **Don't Do This:** Rely on implicit state or hidden side effects, which can lead to unpredictable behavior and make debugging difficult. **Why:** Explicit state management improves code readability, testability, and maintainability. It makes it easier to understand how data changes over time and how different parts of the application interact. ### 1.2. Minimize Mutable State * **Do This:** Design your application to minimize mutable state. If possible, favor immutable data structures, especially when dealing with shared data in concurrent environments. * **Don't Do This:** Mutate shared state directly without proper synchronization, leading to race conditions and data corruption. **Why:** Minimizing mutable state simplifies reasoning about the application's behavior, especially in concurrent contexts. Immutable data structures reduce the likelihood of bugs and improve overall application reliability. ### 1.3. Clear Data Flow * **Do This:** Establish a clearly defined data flow within your application. Consider using architectural patterns like unidirectional data flow or event-driven architectures. * **Don't Do This:** Let data flow become entangled and hard to trace, as this can make debugging and understanding the application very challenging. **Why:** A clear data flow makes it easier to understand how data moves through the system, how different components depend on each other, and how changes in one part of the system affect others. ### 1.4. Context Awareness * **Do This:** Leverage Go's "context.Context " to manage request-scoped state, cancellation signals, and deadlines. * **Don't Do This:** Pass data implicitly through global variables or closures without using context, as this can lead to unpredictable behavior and difficult debugging. **Why:** Using "context.Context" provides a standardized way to manage request lifecycle, propagate cancellation signals, and carry request-scoped values throughout the application. It is a pivotal tool to write well behaved and maintainable applications. ### 1.5. Idempotency * **Do This:** Design critical state transitions to be idempotent when possible. This means repeated application of the same operation should have the same result as a single application. * **Don't Do This:** Create operations that are not idempotent which can lead to issues when retrying failed operations. **Why:** Idempotency guarantees reliability, especially within distributed systems where network failures are common. By ensuring operations are idempotent, retries can be implemented without causing unpredictable state changes. ## 2. Architectural Approaches ### 2.1. Unidirectional Data Flow * **Description:** Components only receive data through one specific way/direction, and events follow to update the state. * **When to use:** Ideal for frontends, UIs, and applications with complex state transformations and dependencies that benefit from predictability. """go package main import "fmt" // Define application state type AppState struct { Count int } // Action specifies the type of state update. type Action string const ( Increment Action = "INCREMENT" Decrement Action = "DECREMENT" ) // Dispatch function to update the state based on actions. func Dispatch(state AppState, action Action) AppState { switch action { case Increment: state.Count++ case Decrement: state.Count-- default: fmt.Println("Unknown action") } return state } func main() { // Initial state state := AppState{Count: 0} // Dispatch actions state = Dispatch(state, Increment) fmt.Println("After increment:", state) state = Dispatch(state, Increment) fmt.Println("After second increment:", state) state = Dispatch(state, Decrement) fmt.Println("After decrement:", state) } """ ### 2.2. Event-Driven Architecture * **Description:** Components communicate through events. State changes trigger events, which are then consumed by other components. * **When to use:** Suitable for microservices, distributed systems, and applications where decoupled components need to react to state changes. """go package main import ( "fmt" "sync" ) // Event type type Event struct { Type string Data interface{} } // EventBus to manage event subscriptions and publishing. type EventBus struct { subscriptions map[string][]chan Event mu sync.Mutex } // NewEventBus creates a new EventBus func NewEventBus() *EventBus { return &EventBus{ subscriptions: make(map[string][]chan Event), } } // Subscribe to an event type. func (eb *EventBus) Subscribe(eventType string, ch chan Event) { eb.mu.Lock() defer eb.mu.Unlock() eb.subscriptions[eventType] = append(eb.subscriptions[eventType], ch) } // Publish an event. func (eb *EventBus) Publish(event Event) { eb.mu.RLock() defer eb.mu.RUnlock() for _, ch := range eb.subscriptions[event.Type] { go func(channel chan Event) { channel <- event }(ch) } } func main() { bus := NewEventBus() // Subscriber 1 subscriber1 := make(chan Event, 1) bus.Subscribe("user.created", subscriber1) go func() { for event := range subscriber1 { fmt.Printf("Subscriber 1: Received event - Type: %s, Data: %+v\n", event.Type, event.Data) } }() // Subscriber 2 subscriber2 := make(chan Event, 1) bus.Subscribe("user.created", subscriber2) go func() { for event := range subscriber2 { fmt.Printf("Subscriber 2: Received event - Type: %s, Data: %+v\n", event.Type, event.Data) } }() // Publish an event bus.Publish(Event{ Type: "user.created", Data: map[string]interface{}{ "userID": "123", "username": "john.doe", }, }) // Keep the program running long enough to receive the event fmt.Scanln() } """ **Anti-pattern:** Tightly coupled services that directly call one another. Event driven architectures can improve loose coupling and scalability. ### 2.3. CQRS (Command Query Responsibility Segregation) * **Description:** Separates read and write operations. Commands modify the state, and queries read the state. * **When to use:** Suitable for complex applications with high read/write loads, where optimizing for both operations simultaneously is challenging. """go package main import "fmt" // Command interface for operations that modify the state type Command interface { Execute() error } // Query interface for operations that read the state type Query interface { Execute() (interface{}, error) } // UserRepository holds the user data type UserRepository struct { users map[string]string } // NewUserRepository creates a new UserRepository func NewUserRepository() *UserRepository { return &UserRepository{ users: make(map[string]string), } } // CreateUserCommand to create a new user type CreateUserCommand struct { repo *UserRepository userID string userName string } // Execute the CreateUserCommand func (c *CreateUserCommand) Execute() error { c.repo.users[c.userID] = c.userName fmt.Printf("User created: ID=%s, Name=%s\n", c.userID, c.userName) return nil } // GetUserQuery to retrieve a user by ID type GetUserQuery struct { repo *UserRepository userID string } // Execute the GetUserQuery func (q *GetUserQuery) Execute() (interface{}, error) { userName, ok := q.repo.users[q.userID] if !ok { return nil, fmt.Errorf("user not found with ID: %s", q.userID) } return userName, nil } func main() { repo := NewUserRepository() // Create user command createUserCmd := &CreateUserCommand{ repo: repo, userID: "123", userName: "john.doe", } createUserCmd.Execute() // Query user getUserQuery := &GetUserQuery{ repo: repo, userID: "123", } userName, err := getUserQuery.Execute() if err != nil { fmt.Printf("Error: %s\n", err) return } fmt.Printf("User name: %s\n", userName) // Query non-existent user getUserQuery = &GetUserQuery{ repo: repo, userID: "456", } userName, err = getUserQuery.Execute() if err != nil { fmt.Printf("Error: %s\n", err) return } fmt.Printf("User name: %s\n", userName) } """ ## 3. Local State Management ### 3.1. Structs for State Representation * **Do This:** Use structs to encapsulate state, which are well-defined and explicitly typed fields. """go type User struct { ID int Name string Email string } type Order struct { ID int UserID int Items []string Total float64 CreatedAt time.Time } """ * **Don't Do This:** Rely on maps or interfaces to represent state when a struct would provide better type safety and structure. * **Why:** Structs offer better type safety, compile-time checks, and improved readability compared to generic data structures. This leads to fewer runtime errors and easier debugging. ### 3.2. Encapsulation and Access Control * **Do This:** Encapsulate state within a struct and provide methods to access and modify it in a controlled manner. Use unexported fields to hide implementation details. """go type Counter struct { mu sync.Mutex count int } func (c *Counter) Increment() { c.mu.Lock() defer c.mu.Unlock() c.count++ } func (c *Counter) Value() int { c.mu.Lock() defer c.mu.Unlock() return c.count } """ * **Don't Do This:** Directly expose state fields, allowing unrestricted access and modification from outside the struct. * **Why:** Encapsulation improves code modularity, reduces dependencies, and simplifies reasoning about the application's state. ### 3.3. Avoiding Global State * **Do This:** Minimize usage of global variables. Instead, pass state explicitly between components or inject dependencies. * **Don't Do This:** Heavily rely on global variables. This introduces tight coupling, makes testing difficult, and can cause unpredictable behavior. ## 4. Concurrent State Management ### 4.1. Mutexes for Protecting Shared State * **Do This:** Use "sync.Mutex" or "sync.RWMutex" to protect shared mutable state when multiple goroutines access the same data. """go package main import ( "fmt" "sync" "time" ) // Counter example with mutex type Counter struct { mu sync.Mutex value int } // Increment method of the counter func (c *Counter) Increment() { c.mu.Lock() defer c.mu.Unlock() c.value++ } // Value method of the counter func (c *Counter) Value() int { c.mu.Lock() defer c.mu.Unlock() return c.value } func main() { var counter Counter var wg sync.WaitGroup // incrementing the counter from multiple goroutines for i := 0; i < 1000; i++ { wg.Add(1) go func() { defer wg.Done() counter.Increment() }() } wg.Wait() fmt.Printf("Counter value: %d\n", counter.Value()) time.Sleep(2 * time.Second) } """ * **Don't Do This:** Access shared mutable state without proper synchronization, leading to race conditions and data corruption. * **Why:** Mutexes ensure that only one goroutine can access the shared state at a time, preventing data races and ensuring data integrity. RWMutex allow multiple readers or one writer. ### 4.2. Channels for Communication and Synchronization * **Do This:** Use channels to communicate and synchronize between goroutines, especially when passing ownership of data. """go package main import ( "fmt" "time" ) func main() { // Create a channel to pass data between goroutines dataChannel := make(chan string) // Launch a goroutine to produce data go func() { time.Sleep(1 * time.Second) dataChannel <- "Hello from goroutine!" }() // Receive data from the channel data := <-dataChannel fmt.Println("Received:", data) time.Sleep(2 * time.Second) } """ * **Don't Do This:** Share memory directly between goroutines without synchronization mechanisms like channels or mutexes. * **Why:** Channels provide a safe and efficient way to pass data between goroutines, avoiding race conditions and ensuring synchronization. ### 4.3. Atomic Operations for Simple State Updates * **Do This:** Use "atomic" package functions for simple, atomic state updates like incrementing counters or flipping boolean flags. """go package main import ( "fmt" "sync/atomic" ) func main() { var counter int64 atomic.AddInt64(&counter, 1) fmt.Println("Counter:", atomic.LoadInt64(&counter)) } """ * **Don't Do This:** Use mutexes for simple atomic operations when the "atomic" package provides a more efficient solution. * **Why:** Atomic operations are faster and more lightweight than mutexes for simple state updates, reducing contention and improving performance. ### 4.4. Read-Copy-Update (RCU) * **Description**: Allows multiple readers to access data concurrently without locks, while updates involve copying the data, modifying the copy, and then atomically swapping the pointer * **When to use:** Best for scenarios with high read concurrency, where updates are less frequent. """go package main import ( "fmt" "sync/atomic" "time" "unsafe" ) // SharedData holds our data that will be accessed using RCU. type SharedData struct { Value string } func main() { // Initialize shared data with an initial value. initialData := &SharedData{Value: "Initial Value"} sharedDataPtr := atomic.Pointer[SharedData]{} sharedDataPtr.Store(initialData) // Reader goroutine go func() { for { dataPtr := sharedDataPtr.Load() data := *dataPtr // Dereference the pointer to get the data fmt.Println("Reader:", data.Value) time.Sleep(100 * time.Millisecond) } }() // Updater goroutine go func() { counter := 1 for { // Create a new copy of the data with the updated value. newData := &SharedData{Value: fmt.Sprintf("Value %d", counter)} // Atomically swap the old data pointer with the new data pointer. atomic.CompareAndSwapPointer[SharedData]( (*unsafe.Pointer)(unsafe.Pointer(&sharedDataPtr)), unsafe.Pointer(sharedDataPtr.Load()), unsafe.Pointer(newData), ) fmt.Println("Updater: Updated value to", newData.Value) counter++ time.Sleep(1 * time.Second) } }() // Keep main running time.Sleep(10 * time.Second) } """ ## 5. Data Persistence State Management ### 5.1. ORM Considerations * **Do This:** Use ORMs like GORM or xorm for managing state persistence with relational databases. Understand the trade-offs of automatic migrations versus manual schema control. * **Don't Do This:** Use ORMs without considering the performance implications of abstracting away direct SQL control. ### 5.2. NoSQL Options * **Do This:** Use NoSQL databases such as MongoDB or Redis (for cache scenarios) to manage unstructured or semi-structured state. * **Don't Do This:** Overuse NoSQL databases for relational data, or ignore ACID properties where necessary. ### 5.3. Caching * **Do This:** Implement caching layers (using packages like "groupcache" or Redis) to improve read performance and reduce database load. * **Don't Do This:** Neglect cache invalidation strategies, which can lead to stale data and application inconsistency. ### 5.4. Transactions * **Do This:** Use transactions (with proper isolation levels) to ensure atomicity, consistency, isolation, and durability (ACID) properties of state changes. * **Don't Do This:** Perform complex operations that must be atomic without using transactions which can lead to data inconsistencies and data corruption. ## 6. Error Handling ### 6.1. Context-Aware Error Handling * **Do This:** Ensure that error handling considers the context in which the error occurred. When an error happens within a goroutine, it should properly propagate the error to a main routine or a designated error handler. """go package main import ( "context" "errors" "fmt" "time" ) func main() { // Create a background context ctx := context.Background() // Create a channel to pass errors errChan := make(chan error, 1) // Buffered channel to avoid blocking // Launch a goroutine that might return an error. go func() { defer close(errChan) // Close the channel when the goroutine completes. // Simulate some work that might result in an error if err := doSomeWork(ctx); err != nil { errChan <- fmt.Errorf("work failed: %w", err) // Send wrapped the error. return } fmt.Println("Work completed successfully.") }() // Wait for the goroutine to complete or for an error to be sent. select { case err := <-errChan: if err != nil { fmt.Printf("Error received: %v\n", err) return // Exit if there's an error } case <-time.After(2 * time.Second): fmt.Println("Work timed out or completed successfully.") } // Continue with the main execution fmt.Println("Continuing with main execution.") } func doSomeWork(ctx context.Context) error { // Simulate work that takes time time.Sleep(1 * time.Second) // Simulate the possibility of an error return errors.New("something went wrong") } """ * **Don't Do This:** Ignore errors originating from separate goroutines, leading to undetected issues and potential state corruption. ### 6.2. Graceful Shutdowns * **Do This:** Implement graceful shutdowns to allow in-flight operations to complete before the application terminates. This prevents data loss and ensures a clean shutdown. """go package main import ( "context" "fmt" "os" "os/signal" "sync" "syscall" "time" ) func main() { // Create a context that listens for OS interrupt signals ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() // Ensure the stop function is called when the main function exits var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() // Simulate a worker that does some work worker(ctx, "worker1") }() wg.Add(1) go func() { defer wg.Done() // Simulate a worker that does some work worker(ctx, "worker2") }() // Wait for shutdown signal or workers to complete <-ctx.Done() fmt.Println("Shutting down gracefully...") // Allow workers some time to complete time.Sleep(3 * time.Second) // Wait for all workers to complete wg.Wait() fmt.Println("Shutdown complete.") } // worker simulates a task that runs until the context is cancelled. func worker(ctx context.Context, name string) { ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() for { select { case <-ctx.Done(): fmt.Printf("%s: Received shutdown signal.\n", name) // Perform any cleanup operations here fmt.Printf("%s: Cleaning up...\n", name) time.Sleep(2 * time.Second) // Simulate cleanup operations fmt.Printf("%s: Exiting.\n", name) return // Exit the worker case t := <-ticker.C: fmt.Printf("%s: Running at %v\n", name, t) } } } """ * **Don't Do This:** Terminate the application abruptly, potentially leaving data in an inconsistent state. ## 7. Testing ### 7.1. Unit Tests * **Do This:** Write unit tests to verify the correctness of individual components and state transitions. """go package mypackage import "testing" func TestCounterIncrement(t *testing.T) { c := &Counter{} c.Increment() if c.Value() != 1 { t.Errorf("Expected counter to be 1, got %d", c.Value()) } } """ ### 7.2. Integration Tests * **Do This:** Implement integration tests to ensure that different components interact correctly and that state is properly managed across module boundaries. ### 7.3. Property-Based Testing * **Do This:** Employ property-based testing libraries like "gopter" to generate a wide range of inputs and verify that state-related properties hold. """go package mypackage import ( "testing" ) func TestCounterValueAlwaysNonNegative(t *testing.T) { c := &Counter{} for i := 0; i < 100; i++ { c.Increment() if c.Value() < 0 { t.Errorf("Counter value should always be non-negative, got %d", c.Value()) } } } """ ## 8. Security ### 8.1. Input Validation * **Do This:** Validate all user inputs to prevent injection attacks and ensure data integrity. ### 8.2. Least Privilege * **Do This:** Grant only the necessary permissions to access and modify state. ### 8.3. Secure Storage * **Do This:** Securely store sensitive data using appropriate encryption techniques and access control mechanisms. """go import ( "crypto/aes" "crypto/cipher" "crypto/rand" "encoding/base64" "io" ) // encryptString encrypts the given string using AES encryption. func encryptString(key []byte, plaintext string) (string, error) { // Create the AES cipher block, err := aes.NewCipher(key) if err != nil { return "", err } // Create a unique ciphertext aesGCM, err := cipher.NewGCM(block) if err != nil { return "", err } // Create a nonce nonce := make([]byte, aesGCM.NonceSize()) if _, err = io.ReadFull(rand.Reader, nonce); err != nil { return "", err } // Encrypt the plaintext ciphertext := aesGCM.Seal(nonce, nonce, []byte(plaintext), nil) return base64.StdEncoding.EncodeToString(ciphertext), nil } // decryptString decrypts the given string using AES encryption. func decryptString(key []byte, encryptedString string) (string, error) { // Decode the encrypted string from base64 enc, err := base64.StdEncoding.DecodeString(encryptedString) if err != nil { return "", err } // Create the AES cipher block, err := aes.NewCipher(key) if err != nil { return "", err } // Create a unique ciphertext aesGCM, err := cipher.NewGCM(block) if err != nil { return "", err } // Get the nonce size nonceSize := aesGCM.NonceSize() // Extract the nonce from the ciphertext nonce, ciphertext := enc[:nonceSize], enc[nonceSize:] // Decrypt the ciphertext plaintext, err := aesGCM.Open(nil, nonce, ciphertext, nil) if err != nil { return "", nil } return string(plaintext), nil } """ ## 9. Performance Optimization ### 9.1. Avoid Unnecessary Copying * **Do This:** Avoid unnecessary copying of large data structures, especially when passing state between goroutines. ### 9.2. Efficient Data Structures * **Do This:** Use efficient data structures appropriate for the task at hand, such as maps, sets, and arrays. ### 9.3. Profiling and Benchmarking * **Do This:** Use Go's profiling and benchmarking tools to identify performance bottlenecks and optimize state management operations. By following these standards, Go developers can build robust, maintainable, and performant applications with well-defined and easily manageable state. These guidelines provide a solid foundation for collaboration and ensure consistent code quality across projects.
# Component Design Standards for Go This document outlines coding standards focused on *Component Design* in Go, providing guidelines for creating reusable, maintainable software components. These standards aim to improve code clarity, reduce complexity, and promote consistency across projects. ## 1. Architectural Principles ### 1.1. Single Responsibility Principle (SRP) * **Do This:** Ensure each component has one, and only one, reason to change. * **Don't Do This:** Create "God" components that handle multiple unrelated responsibilities. **Why:** SRP reduces the likelihood of unintended side effects when modifying a component. It also makes components easier to understand, test, and reuse. **Example:** """go // Good: Separate component for user authentication. package auth type Service struct { // ... } func (s *Service) Authenticate(username, password string) (bool, error) { // ... authentication logic return true, nil } // Bad: Mixing authentication with user profile management in the same component. package user type UserService struct { // ... } func (s *UserService) GetProfile(userID string) (Profile, error) { // ... profile retrieval logic return Profile{}, nil } func (s *UserService) Authenticate(username, password string) (bool, error) { // ... authentication logic should be in a dedicated auth component instead return true, nil } """ ### 1.2. Open/Closed Principle (OCP) * **Do This:** Design components that are open for extension but closed for modification. Use interfaces and composition. * **Don't Do This:** Modify existing component code to add new functionality, which can introduce regressions. **Why:** OCP promotes stability and reduces the risk of breaking existing functionality when adding new features. **Example:** """go // Good: Using an interface for different notification methods. package notification type Notifier interface { Send(message string) error } type EmailNotifier struct { EmailAddress string } func (e EmailNotifier) Send(message string) error { // ... send email return nil } type SMSNotifier struct { PhoneNumber string } func (s SMSNotifier) Send(message string) error { // ... send SMS return nil } type NotificationService struct { Notifier Notifier } func (n *NotificationService) SendNotification(message string) error { return n.Notifier.Send(message) } // To add a new notification method (e.g., Slack), implement the Notifier interface. // No modification to existing code is needed. type SlackNotifier struct { WebhookURL string } func (s SlackNotifier) Send(message string) error { // ...send to slack return nil } // Bad: Modifying the SendNotification function directly to handle multiple notification types. package notification func SendNotification(message string, method string) error { if method == "email" { // ... send email return nil } else if method == "sms" { // ... send SMS return nil } return fmt.Errorf("invalid notification method") } """ ### 1.3. Liskov Substitution Principle (LSP) * **Do This:** Ensure subclasses are substitutable for their base classes without altering correctness. * **Don't Do This:** Create subclasses that violate the contracts of their base classes, leading to unexpected behavior. **Why:** LSP guarantees that any component using a base class can also use its subclasses without any adverse effects. **Example:** """go // Good: A rectangle and a square both implement a Shape interface consistently. package shapes type Shape interface { Area() float64 } type Rectangle struct { Width float64 Height float64 } func (r Rectangle) Area() float64 { return r.Width * r.Height } type Square struct { Side float64 } func (s Square) Area() float64 { return s.Side * s.Side } // Bad: A square modifying Rectangle’s behavior in unexpected ways, breaking the contract of a "general" rectangle where height != width is possible. package shapes type Rectangle struct { Width float64 Height float64 } func (r Rectangle) Area() float64 { return r.Width * r.Height } type Square struct { Side float64 } func (s Square) Area() float64 { return s.Side * s.Side } func (s *Square) SetWidth(width float64) { s.Side = width } func (s *Square) SetHeight(height float64) { s.Side = height } //Using Rectangle methods SetWidth and SetHeight for the square could lead to unintended effects. """ ### 1.4. Interface Segregation Principle (ISP) * **Do This:** Prefer many client-specific interfaces to one general-purpose interface. * **Don't Do This:** Force components to implement methods they don't need. **Why:** ISP reduces dependencies and increases flexibility by allowing components to implement only the interfaces they need. **Example:** """go // Good: Separate interfaces for reading and writing. package io type Reader interface { Read(p []byte) (n int, err error) } type Writer interface { Write(p []byte) (n int, err error) } // Bad: A single interface combining reading and writing. package io type ReadWriter interface { Read(p []byte) (n int, err error) Write(p []byte) (n int, err error) } """ ### 1.5. Dependency Inversion Principle (DIP) * **Do This:** Depend on abstractions (interfaces), not concretions (concrete types). High-level components should not depend on low-level components. Both should depend on abstractions. * **Don't Do This:** Hard-code dependencies between components, making them tightly coupled and difficult to test or replace. **Why:** DIP promotes loose coupling, making components more flexible, testable, and reusable. **Example:** """go // Good: High-level component (OrderProcessor) depends on an abstraction (PaymentGateway). package payment type PaymentGateway interface { Charge(amount float64) error } type StripeGateway struct { APIKey string } func (s StripeGateway) Charge(amount float64) error { // ... charge the amount using Stripe API return nil } type OrderProcessor struct { Gateway PaymentGateway } func (o *OrderProcessor) ProcessOrder(amount float64) error { return o.Gateway.Charge(amount) } // Bad: High-level component directly depends on a concrete type (StripeGateway). package payment type StripeGateway struct { APIKey string } func (s StripeGateway) Charge(amount float64) error { // ... charge the amount using Stripe API return nil } type OrderProcessor struct { Gateway StripeGateway // Hard dependency on StripeGateway } func (o *OrderProcessor) ProcessOrder(amount float64) error { return o.Gateway.Charge(amount) } """ ## 2. Component Structure ### 2.1. Package Organization * **Do This:** Organize code into logical packages. Aim for packages that are small, focused, and cohesive. * **Don't Do This:** Create large, monolithic packages containing unrelated code. This reduces understandability and reusability. **Why:** Proper package organization promotes modularity, reduces coupling, and makes it easier to navigate and maintain code. **Example:** """ project/ ├── cmd/ // main applications │ └── api/ // API server application │ └── worker/ // Background worker application ├── internal/ // private application and library code │ ├── auth/ // Authentication-related components │ ├── database/ // Database access components │ └── models/ // Data models ├── pkg/ // externally usable libraries (optional) └── ... """ ### 2.2. Naming Conventions * **Do This:** Use descriptive and meaningful names for packages, types, functions, and variables. Follow Go's conventions for capitalization and visibility. * **Don't Do This:** Use single-letter or cryptic names that are hard to understand. **Why:** Clear naming improves code readability and maintainability. **Example:** """go package user // Good: Clear and descriptive names. type UserService struct { Repository UserRepository } func (s *UserService) CreateUser(user User) (string, error) { // ... return userID, nil } // Bad: Cryptic names. package user type US struct { Repo UR } func (s *US) CU(usr U) (string, error) { //... return id, nil } """ ### 2.3. Code Documentation * **Do This:** Write clear and concise godoc comments for all packages, types, functions, and methods. * **Don't Do This:** Skip documentation or write vague and uninformative comments. **Why:** Documentation is essential for understanding how components work and how to use them correctly. Godoc is a first-class citizen in the Go ecosystem. Be sure to include examples where appropriate. **Example:** """go // Package user provides functionalities for creating, retrieving, and managing user accounts. package user // UserService provides methods for interacting with user data. type UserService struct { Repository UserRepository } // CreateUser creates a new user account. // It returns the user ID of the newly created user. func (s *UserService) CreateUser(user User) (string, error) { // ... return userID, nil } """ ## 3. Component Implementation ### 3.1. Error Handling * **Do This:** Use Go's error handling mechanism (returning "error" values). Handle errors explicitly and provide informative error messages. Use "errors.Is" and "errors.As" introduced in Go 1.13 for error wrapping and unwrapping and sentinel errors. * **Don't Do This:** Ignore errors, panic unnecessarily, or return generic error messages that provide no context. **Why:** Proper error handling prevents unexpected program termination and helps diagnose and resolve issues. **Example:** """go package repository import ( "errors" "fmt" ) var ErrUserNotFound = errors.New("user not found") func (r *UserRepository) GetUserByID(id string) (User, error) { user, err := r.db.GetUser(id) if err != nil { if errors.Is(err, sql.ErrNoRows) { return User{}, fmt.Errorf("GetUserByID: %w", ErrUserNotFound) } return User{}, fmt.Errorf("failed to get user: %w", err) } return user, nil } func processUser(repo *UserRepository, userID string) error { _, err := repo.GetUserByID(userID) if err != nil { if errors.Is(err, ErrUserNotFound) { // handle user not found error fmt.Println("user not found") return err // or return nil if not an issue } else { // Log unexpected error fmt.Println("unexpected error") return err } } return nil } """ ### 3.2. Concurrency * **Do This:** Use goroutines and channels to handle concurrent operations. Use synchronization primitives (e.g., "sync.Mutex", "sync.WaitGroup") to protect shared resources. Consider using the "context" package for managing request lifecycles and cancellation. * **Don't Do This:** Introduce data races or deadlocks by improperly managing concurrency. **Why:** Concurrency allows for efficient use of system resources and improves application responsiveness. **Example:** """go package worker import ( "fmt" "sync" ) func processTask(task string) { fmt.Printf("Processing task: %s\n", task) // ... perform task } func ProcessTasks(tasks []string) { var wg sync.WaitGroup for _, task := range tasks { wg.Add(1) go func(t string) { defer wg.Done() processTask(t) }(task) } wg.Wait() } """ ### 3.3. Testing * **Do This:** Write unit tests for all components. Use mocking frameworks (e.g., "gomock") to isolate components and test their behavior. Aim for high code coverage. Use table-driven tests for parameterized testing. * **Don't Do This:** Skip testing or write superficial tests that don't adequately verify the functionality of components. Rely on end-to-end tests only. **Why:** Testing ensures that components function correctly and reduces the risk of introducing bugs. **Example:** """go package calculator import "testing" func TestAdd(t *testing.T) { testCases := []struct { a, b int expected int }{ {1, 2, 3}, {0, 0, 0}, {-1, 1, 0}, } for _, tc := range testCases { result := Add(tc.a, tc.b) if result != tc.expected { t.Errorf("Add(%d, %d) = %d; expected %d", tc.a, tc.b, result, tc.expected) } } } """ ### 3.4. Configuration * **Do This:** Externalize configuration using environment variables, configuration files (e.g., YAML, JSON), or command-line arguments. Use libraries like "viper" or "envconfig" to simplify configuration management. * **Don't Do This:** Hard-code configuration values in the source code. **Why:** Externalized configuration allows applications to be deployed in different environments without modifying the code. **Example:** """go package config import ( "github.com/spf13/viper" ) type Config struct { DatabaseURL string "mapstructure:"database_url"" Port int "mapstructure:"port"" } func LoadConfig(path string) (Config, error) { viper.SetConfigFile(path) viper.SetConfigType("yaml") // or json, toml viper.AutomaticEnv() err := viper.ReadInConfig() if err != nil { return Config{}, err } var config Config err = viper.Unmarshal(&config) return config, err } """ ### 3.5. Logging * **Do This:** Utilize a structured logging library (e.g., "zap", "logrus") to log application events. Include contextual information (e.g., request IDs, user IDs) in logs. Configure logging levels appropriately. * **Don't Do This:** Use "fmt.Println" for logging. Don't log sensitive information (e.g., passwords, API keys) directly. **Why:** Logging provides valuable insights into application behavior and simplifies troubleshooting. **Example:** """go package main import ( "go.uber.org/zap" ) func main() { logger, _ := zap.NewProduction() // or zap.NewDevelopment() for development environment defer logger.Sync() // flushes buffer, if any sugar := logger.Sugar() url := "http://example.com" sugar.Infow("Failed to fetch URL.", // Structured context as key-value pairs. "url", url, "attempt", 3, "backoff", "3s", ) sugar.Infof("Failed to fetch URL: %s", url) } """ ## 4. Design Patterns ### 4.1. Factory Pattern * **Use Case:** Abstract object creation logic to improve flexibility and testability. * **Go Implementation:** Define an interface for the created objects and a factory function that returns instances of types implementing the interface. """go package factory type Car interface { Drive() string } type Tesla struct{} func (t Tesla) Drive() string { return "Driving a Tesla" } type BMW struct{} func (b BMW) Drive() string { return "Driving a BMW" } type CarFactory struct{} func (cf CarFactory) CreateCar(carType string) Car { switch carType { case "tesla": return Tesla{} case "bmw": return BMW{} default: return nil } } """ ### 4.2. Strategy Pattern * **Use Case:** Define a family of algorithms, encapsulate each one, and make them interchangeable. Lets the algorithm vary independently from clients that use it. * **Go Implementation:** Define an interface for the strategies and have different implementations of that interface for each algorithm. """go package strategy type PaymentStrategy interface { Pay(amount int) string } type CreditCardPayment struct { CardNumber string } func (c CreditCardPayment) Pay(amount int) string { return "Paid $" + string(amount) + " with credit card" } type PayPalPayment struct { Email string } func (p PayPalPayment) Pay(amount int) string { return "Paid $" + string(amount) + " with PayPal" } type ShoppingCart struct { Payment PaymentStrategy } func (s ShoppingCart) Checkout(amount int) string { return s.Payment.Pay(amount) } """ ### 4.3. Observer Pattern * **Use Case:** Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. * **Go Implementation:** Use interfaces for subjects and observers. The subject maintains a list of observers and notifies them of state changes. """go package observer type Observer interface { Update(message string) } type Subject interface { Attach(observer Observer) Detach(observer Observer) Notify(message string) } type ConcreteSubject struct { observers []Observer } func (s *ConcreteSubject) Attach(observer Observer) { s.observers = append(s.observers, observer) } func (s *ConcreteSubject) Detach(observer Observer) { // Remove from the list. More robust version could search. s.observers = s.observers[:len(s.observers)-1] } func (s *ConcreteSubject) Notify(message string) { for _, observer := range s.observers { observer.Update(message) } } type ConcreteObserver struct { ID int } func (o *ConcreteObserver) Update(message string) { println(fmt.Sprintf("Observer %d received %s", o.ID, message)) } """ ## 5. Performance Considerations ### 5.1. Memory Management * **Do This:** Be mindful of memory allocation and deallocation, especially in performance-critical sections of the code. Use the "sync.Pool" for reusing objects. Understand the garbage collector's behavior. * **Don't Do This:** Create unnecessary object allocations. **Why:** Efficient memory management improves application performance and reduces resource consumption. ### 5.2. Data Structures and Algorithms * **Do This:** Choose appropriate data structures and algorithms for the task. Use Go's built-in data structures (e.g., slices, maps) effectively. * **Don't Do This:** Use inefficient data structures or algorithms that lead to poor performance. ### 5.3. String Operations * **Do This:** Use efficient string manipulation techniques. Use the "strings.Builder" for building strings incrementally. * **Don't Do This:** Perform inefficient string concatenation using the "+" operator in loops. ## 6. Security Best Practices ### 6.1. Input Validation * **Do This:** Validate all user inputs to prevent injection attacks (e.g., SQL injection, XSS). * **Don't Do This:** Trust user inputs blindly. ### 6.2. Authentication and Authorization * **Do This:** Implement robust authentication and authorization mechanisms. Use established libraries (e.g., "bcrypt", "jwt-go"). * **Don't Do This:** Store passwords in plain text. Don't rely on weak authentication schemes. ### 6.3. Data Encryption * **Do This:** Encrypt sensitive data both in transit (using TLS) and at rest (using encryption algorithms like AES). ### 6.4. Dependency Management * **Do This:** Use Go modules for dependency management. Regularly audit dependencies for vulnerabilities. Consider using tools like "govulncheck". * **Don't Do This:** Use outdated or vulnerable dependencies. ## 7. Evolving Standards These standards are living documents and will evolve as the Go language and best practices improve. Regularly review and update these standards to reflect the latest advancements in the Go ecosystem. Community feedback is encouraged. All updates will be driven by concrete experience and measurement.