13. Packages and Modules
π¦ Master Go packages and modules! Learn package organization, imports, visibility rules, dependency management with go.mod, and building reusable code libraries.
What we will learn in this post?
- π Package Basics
- π Importing Packages
- π Exported vs Unexported
- π Go Modules
- π Managing Dependencies
- π Creating Your Own Packages
- π Conclusion!
π¦ Go Packages: Organizing Your Code!
Go packages are the foundation of code organization in production applications, enabling modular design and reusable libraries. Think of them as digital folders that group related functions, variables, and types together. This makes your code easier to read, reuse, and maintain!
π Package Naming Rules
When naming your packages, remember these simple rules:
- Always
lowercase: Use only lowercase letters. - No
underscores: Avoid using_in package names. - Descriptive: Choose a name that clearly describes what the package does (e.g.,
myutils,database,httpclient). - The special
mainpackage is reserved for programs that can be directly executed.
β¬οΈ Declaring Your Package
Every single Go file must declare its package. You do this using the package keyword at the very top of the file.
1
2
3
4
5
// For an executable program
package main
// For a reusable library package
// package mypackage
π€ One Directory, One Package
This is a golden rule! All .go files located within the same directory must declare the same package name. Go uses this structure to understand how your project is organized.
For example, if you have myproject/utils/helper.go and myproject/utils/formatter.go, both files must start with package utils.
π‘ Simple Example Structure
Hereβs how a typical project might look:
graph TD
A["myproject/"] --> B["main.go"]
A --> C["utils/"]
C --> D["utils/helper.go"]
B -- "Declares: package main" --> P1("Executable Program")
D -- "Declares: package utils" --> P2("Reusable Library")
classDef pkgRoot fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14;
classDef mainFile fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14;
classDef pkgDir fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14;
classDef utilFile fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14;
classDef exec fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14;
classDef lib fill:#ff9800,stroke:#f57c00,color:#fff,font-size:16px,stroke-width:3px,rx:14;
class A pkgRoot;
class B mainFile;
class C pkgDir;
class D utilFile;
class P1 exec;
class P2 lib;
linkStyle default stroke:#e67e22,stroke-width:3px;
In this example, main.go is part of package main, making it an executable program. The helper.go file within the utils directory declares package utils, making its functions available for other parts of your project (like main.go) to import and use.
π Further Reading
Go Package Imports: Your Essential Guide! π
Importing packages enables code reuse across large-scale applications and microservices architectures. Itβs crucial for structuring your applications and leveraging Goβs vast standard library and community packages.
Standard Imports π
This is the most common way. You simply declare the package path, and then use its exported functions or variables by prefixing them with the package name.
- Syntax:
import "package_path" - Example:
1 2 3 4 5
import "fmt" // Imports the standard format package func main() { fmt.Println("Hello, Go!") // Using fmt.Println }
Aliasing Imports π·οΈ
You can give a package an alias (a different name). This is useful to avoid name collisions if two packages have the same name, or to shorten a long package name for convenience.
- Syntax:
import alias "package_path" - Example:
1 2 3 4 5
import f "fmt" // Alias "fmt" as "f" func main() { f.Println("Using an alias!") // Using f.Println }
Blank Imports (_) π»
A blank import is used when you need a package for its side effects only, typically because of its init() function. The imported packageβs exported members wonβt be directly used in your code. Common for database drivers.
- Syntax:
import _ "package_path" - Example:
1 2 3 4 5
import _ "github.com/go-sql-driver/mysql" // Driver registers itself in init() func main() { // ... code to use database/sql with mysql driver ... }
Dot Imports (.) π (Discouraged!)
A dot import brings all exported identifiers of a package directly into your current fileβs scope, meaning you donβt need to prefix them with the package name. This is highly discouraged as it can lead to name clashes and make code harder to read and maintain.
- Syntax:
import . "package_path" - Example:
1 2 3 4 5
import . "fmt" // Imports fmt, but now Println can be used directly func main() { Println("No prefix here!") // Println used without "fmt." }
Real-World Import Example: HTTP API Server π
Hereβs how imports work in a production API server:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package main
import (
"encoding/json" // Standard library for JSON handling
"log" // Logging for debugging
"net/http" // HTTP server functionality
"time" // Time operations
"github.com/gorilla/mux" // Popular HTTP router
db "myapp.com/database" // Alias for custom database package
"myapp.com/auth" // Custom authentication package
)
type UserResponse struct {
ID int `json:"id"`
Username string `json:"username"`
CreatedAt time.Time `json:"created_at"`
}
func getUserHandler(w http.ResponseWriter, r *http.Request) {
// Verify authentication
if !auth.IsAuthenticated(r) {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Get user from database using aliased package
user, err := db.GetUser(123)
if err != nil {
log.Printf("Database error: %v", err)
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}
// Create response
response := UserResponse{
ID: user.ID,
Username: user.Name,
CreatedAt: time.Now(),
}
// Encode and send JSON response
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
func main() {
router := mux.NewRouter()
router.HandleFunc("/api/users/{id}", getUserHandler).Methods("GET")
log.Println("Server starting on :8080")
http.ListenAndServe(":8080", router)
}
Import Types Quick Reference π
Hereβs a quick comparison to help you choose the right import style:
| Import Type | Syntax | Use Case | Example | Best For |
|---|---|---|---|---|
| Standard | import "pkg" | Most common, explicit usage | fmt.Println() | β General use, clear code |
| Aliased | import alias "pkg" | Avoid name conflicts, shorten long names | db.Connect() | β Multiple similar packages, long package paths |
| Blank | import _ "pkg" | Side effects only (init functions) | _ "github.com/lib/pq" | β Database drivers, plugins |
| Dot | import . "pkg" | Direct access (discouraged) | Println() | β Avoid - causes confusion |
Pro Tip: Stick to standard imports for 95% of cases. Use aliases when you have multiple packages with similar names (e.g., v1 "myapp/api/v1" and v2 "myapp/api/v2").
Further Reading π
Goβs Visibility Rules: Exported vs. Unexported! π
In Go, a simple yet powerful convention governs identifier visibility: the capitalization of its very first letter. This determines whether your code components are accessible to other packages or remain private, ensuring clear boundaries and good package design.
Understanding the Basics π
- Exported (Public) Identifiers:
- Always start with an uppercase letter (e.g.,
User,GetUser,Name). - They are public and can be accessed and used from any other package that imports yours. Think of these as your packageβs official API.
- Always start with an uppercase letter (e.g.,
- Unexported (Private) Identifiers:
- Always start with a lowercase letter (e.g.,
user,getUser,name). - They are private and are only accessible within the same package where they are defined. These are internal helpers, keeping your packageβs logic encapsulated.
- Always start with a lowercase letter (e.g.,
graph TD
A["Identifier's First Letter"] --> B{"Is it Uppercase?"}
B -- "Yes" --> C["Exported (Public)"]
B -- "No (Lowercase)" --> D["Unexported (Private)"]
C --> E["Accessible from other packages"]
D --> F["Only accessible within its own package"]
classDef start fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14;
classDef decision fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14;
classDef exported fill:#43e97b,stroke:#38f9d7,color:#222,font-size:16px,stroke-width:3px,rx:14;
classDef unexported fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14;
classDef access fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14;
classDef restrict fill:#ff9800,stroke:#f57c00,color:#fff,font-size:16px,stroke-width:3px,rx:14;
class A start;
class B decision;
class C exported;
class D unexported;
class E access;
class F restrict;
linkStyle default stroke:#e67e22,stroke-width:3px;
Examples in Action β¨
Letβs see how this applies to different Go elements:
- Structs:
type Product struct {}(exported, visible to other packages)type productConfig struct {}(unexported, private to its package)
- Struct Fields: (inside a struct like
Product)Name string(exported field, can be accessed by others)cost float64(unexported field, private to its package)
- Functions:
func GetProductData() {}(exported, callable by other packages)func calculateDiscount() {}(unexported, only callable within its package)
Real-World Example: User Authentication Package π
Hereβs how exported and unexported identifiers work together in a production authentication package:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// Package auth provides user authentication functionality
package auth
import (
"crypto/sha256"
"encoding/hex"
"errors"
"time"
)
// User represents an authenticated user (Exported)
type User struct {
ID int // Exported: accessible to other packages
Username string // Exported: accessible to other packages
Email string // Exported: accessible to other packages
password string // Unexported: only accessible within auth package
salt string // Unexported: security measure, private
}
// sessionStore holds active sessions (unexported, internal use only)
type sessionStore struct {
sessions map[string]*User
expiry map[string]time.Time
}
// global session storage (unexported)
var store = &sessionStore{
sessions: make(map[string]*User),
expiry: make(map[string]time.Time),
}
// CreateUser creates a new user with hashed password (Exported - Public API)
func CreateUser(username, email, password string) (*User, error) {
if len(password) < 8 {
return nil, errors.New("password too short")
}
salt := generateSalt() // Call unexported helper
hashedPwd := hashPassword(password, salt) // Call unexported helper
user := &User{
Username: username,
Email: email,
password: hashedPwd,
salt: salt,
}
return user, nil
}
// Authenticate verifies user credentials (Exported - Public API)
func Authenticate(username, password string) (*User, error) {
user, err := findUser(username) // Call unexported helper
if err != nil {
return nil, errors.New("invalid credentials")
}
hashed := hashPassword(password, user.salt)
if hashed != user.password {
return nil, errors.New("invalid credentials")
}
return user, nil
}
// generateSalt creates a random salt (unexported - internal helper)
func generateSalt() string {
// Implementation details hidden from package users
return hex.EncodeToString([]byte(time.Now().String()))[:16]
}
// hashPassword hashes password with salt (unexported - internal helper)
func hashPassword(password, salt string) string {
hasher := sha256.New()
hasher.Write([]byte(password + salt))
return hex.EncodeToString(hasher.Sum(nil))
}
// findUser searches for user by username (unexported - internal helper)
func findUser(username string) (*User, error) {
// In real implementation, this would query a database
return nil, errors.New("user not found")
}
Key Takeaways:
User,CreateUser(),Authenticate()are exported - they form the public APIpassword,salt,sessionStore, helper functions are unexported - implementation details remain hidden- This encapsulation protects sensitive security logic and allows internal changes without breaking client code
This fundamental rule helps you build well-structured and manageable Go packages!
Visibility Rules Comparison π
| Aspect | Exported (Public) | Unexported (Private) |
|---|---|---|
| Naming | Starts with Uppercase | Starts with lowercase |
| Access | Any package can use | Same package only |
| Purpose | Public API, intended for external use | Internal helpers, implementation details |
| Examples | User, GetData(), MaxSize | user, getData(), maxSize |
| Best Use | Functions/types clients need | Helper functions, internal state |
| Documentation | Should have doc comments | Optional documentation |
Design Principle: Start with unexported by default. Only export whatβs necessary for your public API. This gives you flexibility to change internal implementation without breaking client code.
Further Reading π
- For more insights, check out the official Effective Go: Names documentation.
Go Modules: Managing Your Code π¦
What are Go Modules? π€
Go modules, introduced in Go 1.11, revolutionized dependency management in Go. They provide a robust and reproducible way to handle project dependencies, ensuring that your builds are consistent every time. Itβs the standard for modern Go development!
The Core Files: go.mod & go.sum π
go.mod: This essential file, located at your moduleβs root, defines your module path (a unique identifier) and lists all direct and indirect dependencies, specifying their semantic versions.go.sum: This security-focused file contains cryptographic checksums for all your moduleβs dependencies. It verifies integrity, protecting against malicious or accidental changes to downloaded modules.
Semantic Versioning & go mod init π
Go modules leverage Semantic Versioning (e.g., v1.2.3), making it clear what kind of changes a new version introduces (major, minor, patch). To start a new module, simply navigate to your project folder and run go mod init <module-path>. This command initializes your module by creating the go.mod file.
Letβs Create a Module! β¨
Hereβs a quick example to get started:
1
2
3
mkdir my-awesome-app
cd my-awesome-app
go mod init example.com/my-awesome-app
This sequence will generate your initial go.mod file in the my-awesome-app directory.
Module Initialization Flow:
graph TD
A["Start"] --> B["Create Project Directory"]
B --> C["Navigate into Directory"]
C --> D["go mod init <module-path>"]
D --> E["go.mod created"]
E --> F["End"]
classDef start fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14;
classDef process fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14;
classDef command fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14;
classDef success fill:#43e97b,stroke:#38f9d7,color:#222,font-size:16px,stroke-width:3px,rx:14;
classDef endNode fill:#ff9800,stroke:#f57c00,color:#fff,font-size:16px,stroke-width:3px,rx:14;
class A start;
class B,C process;
class D command;
class E success;
class F endNode;
linkStyle default stroke:#e67e22,stroke-width:3px;
Go Modules: Managing Your Projectβs Dependencies π€
Go Modules manage external dependencies in production environments, ensuring reproducible builds across development, staging, and production. They ensure your project builds consistently and reproducibly, making collaboration and deployment a breeze.
Adding New Friends with go get β
When your project needs a new external package, go get is your go-to command. It downloads the required package and adds it (and its version) to your projectβs go.mod file.
- Command:
go get example.com/some/package - Workflow: You often just add an
importstatement to your code, and thengo buildorgo runwill implicitly callgo getto fetch it. Or, explicitly rungo getto fetch a specific version.
Keeping Your Friend List Tidy with go mod tidy β¨
go mod tidy acts like a meticulous organizer! It ensures your go.mod file accurately reflects the dependencies actually used by your project. It adds any missing dependencies and removes unused ones.
- Command:
go mod tidy - Workflow: Run this regularly, especially after adding or removing
importstatements, to keep your dependency list clean and correct.
Bringing Friends Closer with go mod vendor π¦
go mod vendor copies all your projectβs dependencies from your Go Module cache into a local vendor directory within your project.
- Command:
go mod vendor - Why use it? This makes your project self-contained. Itβs great for reproducible builds, especially in environments with limited internet access or strict build policies, as the project no longer needs to fetch dependencies during build time.
Common Workflow Overview π
graph LR
A["Start Coding Go Project"] --> B{"Need a new dependency?"}
B -- "Yes" --> C["Add import statement"]
C --> D["go get new/package"]
D --> E{"Code changes or done?"}
E -- "Yes" --> F["go mod tidy"]
F --> G{"Need to vendor?"}
G -- "Yes" --> H["go mod vendor"]
H --> I["Ready to Build/Deploy! β
"]
B -- "No" --> E
E -- "No" --> A
G -- "No" --> I
classDef start fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14;
classDef decision fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14;
classDef action fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14;
classDef command fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14;
classDef endNode fill:#43e97b,stroke:#38f9d7,color:#222,font-size:16px,stroke-width:3px,rx:14;
class A start;
class B,E,G decision;
class C action;
class D,F,H command;
class I endNode;
linkStyle default stroke:#e67e22,stroke-width:3px;
Real-World Example: Adding Dependencies to a Web API π§
Hereβs a practical example of managing dependencies for a REST API project:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# Initialize a new module
mkdir product-api
cd product-api
go mod init github.com/mycompany/product-api
# Create main.go with needed imports
cat > main.go << 'EOF'
package main
import (
"encoding/json"
"log"
"net/http"
"github.com/gorilla/mux"
"github.com/go-redis/redis/v8"
"gorm.io/gorm"
"gorm.io/driver/postgres"
)
func main() {
log.Println("Starting API server...")
}
EOF
# Add dependencies (downloads and updates go.mod)
go get github.com/gorilla/mux@v1.8.0
go get github.com/go-redis/redis/v8
go get gorm.io/gorm
go get gorm.io/driver/postgres
# Clean up unused dependencies and add missing ones
go mod tidy
# View your go.mod file
cat go.mod
Resulting go.mod file:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module github.com/mycompany/product-api
go 1.21
require (
github.com/go-redis/redis/v8 v8.11.5
github.com/gorilla/mux v1.8.0
gorm.io/driver/postgres v1.5.4
gorm.io/gorm v1.25.5
)
require (
// Indirect dependencies automatically added
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/jackc/pgx/v5 v5.4.3 // indirect
// ... other indirect dependencies
)
For Docker deployments (vendor dependencies):
1
2
3
4
5
6
7
8
9
10
11
12
# Create vendor directory with all dependencies
go mod vendor
# Now your Dockerfile can build offline
cat > Dockerfile << 'EOF'
FROM golang:1.21-alpine
WORKDIR /app
COPY . .
# Build using vendored dependencies (no network needed)
RUN go build -mod=vendor -o api .
CMD ["./api"]
EOF
Key Benefits:
- β Reproducible builds: Same dependency versions across all environments
- β Version pinning: Lock specific versions for stability
- β Offline builds: Vendor mode enables builds without internet
- β
Security:
go.sumverifies dependency integrity
Go Module Commands Reference π―
Hereβs your essential toolkit for managing Go modules:
| Command | Purpose | When to Use | Output |
|---|---|---|---|
go mod init <path> | Initialize new module | Starting a new project | Creates go.mod |
go get <package> | Add/update dependency | Need a new package | Updates go.mod and go.sum |
go mod tidy | Clean up dependencies | After adding/removing imports | Syncs go.mod with actual code |
go mod vendor | Copy deps to vendor/ | Offline builds, air-gapped environments | Creates vendor/ directory |
go mod download | Download dependencies | Pre-populate cache | Downloads to module cache |
go mod verify | Verify checksums | Security audit | Checks go.sum integrity |
go list -m all | List all dependencies | Audit dependency tree | Shows version info |
go mod graph | Show dependency graph | Understand relationships | Prints module graph |
go mod why <package> | Explain dependency | βWhy is this here?β | Shows import path chain |
Common Workflow:
1
2
3
4
5
6
7
8
9
10
11
# Start new project
go mod init github.com/yourname/project
# Add dependencies as you import them
go build # or go run
# Clean up
go mod tidy
# For production deployment
go mod vendor # Optional: vendor dependencies
Further Reading π
- For a comprehensive guide, check out the official Go Modules Reference.
Building Awesome Reusable Packages! π¦
Creating reusable Go packages enables code sharing across microservices and projects. It makes sharing, maintaining, and using your code much easier across teams.
1. Organizing Your Code Internally π§©
Within a package, split your code into multiple .go files based on their specific job. For example, a package myutils/ might have strings.go for string utilities, numbers.go for mathematical functions, and validation.go for input validation. All files in the same directory must declare the same package name. This keeps files tidy and focused!
2. Structuring Your Package Directories π³
A typical Go project structure includes your module root, internal packages, and test files alongside implementation files.
graph TD
A["myproject/"] --> B["go.mod"]
A --> C["go.sum"]
A --> D["main.go"]
A --> E["myutils/"]
E --> F["strings.go"]
E --> G["strings_test.go"]
E --> H["numbers.go"]
E --> I["numbers_test.go"]
A --> J["internal/"]
J --> K["config/"]
K --> L["config.go"]
classDef root fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14;
classDef config fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14;
classDef main fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14;
classDef pkg fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14;
classDef code fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:14px,stroke-width:2px,rx:10;
classDef test fill:#43e97b,stroke:#38f9d7,color:#222,font-size:14px,stroke-width:2px,rx:10;
classDef internal fill:#ff9800,stroke:#f57c00,color:#fff,font-size:16px,stroke-width:3px,rx:14;
class A root;
class B,C config;
class D main;
class E,K pkg;
class F,H,L code;
class G,I test;
class J internal;
linkStyle default stroke:#e67e22,stroke-width:3px;
3. Best Practices & Design Tips β¨
- Clear Purpose: Each package should do one thing really well (Single Responsibility Principle).
- Intuitive APIs: Design your exported functions to be easy to understand and use. Use lowercase for unexported helpers.
- Documentation: Write clear doc comments starting with the function/type name. These appear in
go docand godoc.org. - Testing: Place test files alongside implementation (
*_test.go). Use table-driven tests for comprehensive coverage. - Package Naming: Use short, lowercase, singular names (e.g.,
http,json,time). Avoid underscores or mixedCaps. - Internal Packages: Use
internal/directory for packages that should only be imported by your module.
Real-World Example: String Utilities Package π¨
Hereβs a production-ready reusable package:
File: myutils/strings.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// Package myutils provides common utility functions for string manipulation.
package myutils
import (
"strings"
"unicode"
)
// Capitalize converts the first character of a string to uppercase.
// It returns the original string if empty or already capitalized.
func Capitalize(s string) string {
if s == "" {
return s
}
runes := []rune(s)
runes[0] = unicode.ToUpper(runes[0])
return string(runes)
}
// TruncateWithEllipsis truncates a string to maxLen characters,
// adding "..." if truncated. Used for preview text in UIs.
func TruncateWithEllipsis(s string, maxLen int) string {
if len(s) <= maxLen {
return s
}
if maxLen < 3 {
return "..."
}
return s[:maxLen-3] + "..."
}
// Slugify converts a string to a URL-friendly slug.
// Example: "Hello World!" -> "hello-world"
func Slugify(s string) string {
s = strings.ToLower(s)
s = strings.TrimSpace(s)
// Replace spaces with hyphens
s = strings.ReplaceAll(s, " ", "-")
// Remove special characters
result := make([]rune, 0, len(s))
for _, r := range s {
if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '-' {
result = append(result, r)
}
}
return string(result)
}
// isValidEmail checks if string is a valid email (unexported helper)
func isValidEmail(email string) bool {
return strings.Contains(email, "@") && strings.Contains(email, ".")
}
File: myutils/strings_test.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package myutils
import "testing"
func TestCapitalize(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{"empty string", "", ""},
{"lowercase", "hello", "Hello"},
{"already capitalized", "World", "World"},
{"single char", "a", "A"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Capitalize(tt.input)
if result != tt.expected {
t.Errorf("Capitalize(%q) = %q; want %q",
tt.input, result, tt.expected)
}
})
}
}
func TestSlugify(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"Hello World", "hello-world"},
{"Go Programming!", "go-programming"},
{" Spaces ", "spaces"},
}
for _, tt := range tests {
result := Slugify(tt.input)
if result != tt.expected {
t.Errorf("Slugify(%q) = %q; want %q",
tt.input, result, tt.expected)
}
}
}
Usage in another project:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main
import (
"fmt"
"github.com/yourname/myproject/myutils"
)
func main() {
title := "hello world"
fmt.Println(myutils.Capitalize(title)) // "Hello world"
article := "This is a very long article content..."
preview := myutils.TruncateWithEllipsis(article, 20)
fmt.Println(preview) // "This is a very lo..."
slug := myutils.Slugify("My Blog Post!")
fmt.Println(slug) // "my-blog-post"
}
Remember: A well-designed package is a joy to use!
Package Organization Patterns π
Choose the right organization pattern based on your project needs:
| Pattern | Structure | Use Case | Pros | Cons |
|---|---|---|---|---|
| Flat | All packages at root | Small projects, utilities | β
Simple β Easy to navigate | β Gets messy as project grows |
| Layered | /api, /service, /repo | Web applications | β
Clear separation of concerns β Testable | β Can be over-engineered for small apps |
| Domain-Driven | /user, /order, /payment | Business applications | β
Aligns with business logic β Scalable | β Requires careful planning |
| Standard Layout | /cmd, /pkg, /internal | Large projects, CLIs | β
Industry standard β Clear public vs private | β More boilerplate |
Standard Go Project Layout:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
myproject/
βββ cmd/ # Main applications
β βββ myapp/
β βββ main.go
βββ internal/ # Private application code
β βββ auth/
β βββ database/
βββ pkg/ # Public library code
β βββ utils/
βββ api/ # API definitions (OpenAPI, protobuf)
βββ web/ # Web assets (if applicable)
βββ scripts/ # Build and deploy scripts
βββ docs/ # Documentation
βββ go.mod
βββ go.sum
Key Guidelines:
- Use
internal/for code that should NOT be imported by other projects - Use
pkg/for reusable library code that others can import - Use
cmd/for executable applications - Keep
main.gominimal - it should just wire things together
Conclusion
Youβve now mastered Go packages and modules! From understanding package basics and import patterns to managing dependencies with go.mod and building reusable libraries, you have the tools to organize production-grade Go applications.
Next steps to level up your Go skills:
- π¨ Create a utility package for your common functions and publish it on GitHub
- π¦ Practice dependency management by building a REST API with external packages
- ποΈ Structure a multi-package project following Goβs standard layout conventions
- π§ͺ Write comprehensive tests for your packages using table-driven test patterns
Start applying these patterns in your projects today and watch your codebase become more maintainable and scalable!
What are your thoughts on this topic? Have you built reusable Go packages before? Share your experiences in the comments below! ππ¬β¨