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.