YouTube Summaries | How to Structure Your Golang (API) Projects
December 11th, 2023
Introduction:
This summary will serve to cement the learnings that I took from the video above, which discusses structure GoLang projects, specifically when building an API. I hope you find it useful too!
The outlined project directly structure is as follows:
my-golang-project
|-- main.go
|-- api
| |-- server.go
| |-- handlers.go
|-- types
| |-- user.go
|-- storage
| |-- storage.go
| |-- memory.go
| |-- mongodb.go
|-- util
| |-- util.go
|-- api_test
| |-- server_test.go
Project Structure:
The video suggests a simple project structure with packages such as api, types, storage, and util. The main.go file initializes the server, handles command-line flags, and initializes a storage instance based on the chosen implementation.
Code Example:
package main
import (
"flag"
"fmt"
"log"
"net/http"
"github.com/yourusername/my-golang-project/api"
"github.com/yourusername/my-golang-project/storage"
)
func main() {
// Parse command-line flags
listenAddr := flag.String("listen", ":3000", "Server listen address")
flag.Parse()
// Initialize storage (Choose either memory or mongodb implementation)
store := storage.NewMemoryStorage()
// store := storage.NewMongoDBStorage("mongodb://localhost:27017")
// Initialize the server with the storage instance
server := api.NewServer(*listenAddr, store)
// Start the server
fmt.Printf("Server running on %s\n", *listenAddr)
log.Fatal(http.ListenAndServe(*listenAddr, nil))
}
API Handling:
The api package contains the server and handlers. The Server struct manages HTTP routes, and an example route for fetching a user by ID is demonstrated. The handleUserByID function retrieves user data from storage and responds with a JSON representation.
Code Example:
package api
import (
"fmt"
"net/http"
"github.com/yourusername/my-golang-project/storage"
"github.com/yourusername/my-golang-project/types"
)
type Server struct {
listenAddr string
store storage.Storage
}
func NewServer(listenAddr string, store storage.Storage) *Server {
return &Server{
listenAddr: listenAddr,
store: store,
}
}
func (s *Server) handleUserByID(w http.ResponseWriter, r *http.Request) {
// Example: Get user by ID from storage
user := s.store.Get(1)
// Example: Encode user as JSON and write to response
fmt.Fprintf(w, "{\"id\": %d, \"name\": \"%s\"}", user.ID, user.Name)
}
func (s *Server) Start() error {
// Example: Handle "/user" endpoint
http.HandleFunc("/user", s.handleUserByID)
// Start server
return http.ListenAndServe(s.listenAddr, nil)
}
Type Definitions:
The types package holds type definitions for entities like the User struct, which model data structures throughout the application.
Code Example:
package types
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
Storage Layer:
The storage package provides an interface (Storage) for data storage and specific implementations, such as MemoryStorage. The chosen storage implementation is initialized in the main file. The video highlights the simplicity of the in-memory storage example.
Code Examples:
storage/storage.go
package storage
import "github.com/yourusername/my-golang-project/types"
type Storage interface {
Get(id int) types.User
// Add other storage methods here
}
storage/memory.go:
package storage
import "github.com/yourusername/my-golang-project/types"
type MemoryStorage struct{}
func NewMemoryStorage() *MemoryStorage {
// Initialization logic for memory storage
return &MemoryStorage{}
}
func (m *MemoryStorage) Get(id int) types.User {
// Example: In-memory implementation
return types.User{ID: id, Name: "Foo"}
}
Utility Functions:
A util package is suggested for utility functions used across the application. An example includes a RoundToDecimals function for rounding floating-point numbers.
Code Example:
package util
import "math"
func RoundToDecimals(val float64) float64 {
return math.Round(val*100) / 100
}
Testing:
Testing is encouraged by creating a separate test file (api_test/server_test.go). The video demonstrates a test for the handleUserByID function using Go’s testing package.
Code Example:
package api_test
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/yourusername/my-golang-project/api"
)
func TestHandleUserByID(t *testing.T) {
// Create a test server
server := api.NewServer(":3000", nil)
// Create a request to the "/user" endpoint
req, err := http.NewRequest("GET", "/user", nil)
if err != nil {
t.Fatal(err)
}
// Create a response recorder to record the response
rr := httptest.NewRecorder()
// Call the handler function
server.HandleUserByID(rr, req)
// Check the status code
if status := rr.Code; status != http.StatusOK {
t.Errorf("Handler returned wrong status code: got %v want %v", status, http.StatusOK)
}
// Check the response body (assuming the expected JSON response)
expected := `{"id": 1, "name": "Foo"}`
if rr.Body.String() != expected {
t.Errorf("Handler returned unexpected body: got %v want %v", rr.Body.String(), expected)
}
}
The proposed structure aligns with Go’s simplicity, promoting accessibility for developers and facilitating long-term maintainability.