-
Notifications
You must be signed in to change notification settings - Fork 0
Description
GopherCon 2018: How Do You Structure Your Go Apps - Kat Zien
Introduction: The Perennial Question of Go Application Structure
Kat Zien opens by addressing a common challenge for Go developers, both new and experienced: "How do you structure your Go apps?" She notes that Go, unlike some other languages, doesn't impose a strict structure, leaving developers with a "blank page" and the freedom to experiment. While this flexibility is powerful, it can also be daunting, especially for those new to Go or coming from object-oriented backgrounds.
Why Good Structure Matters
Zien emphasizes the critical importance of a good application structure. Citing Dave Cheney, she states that maintainability and ease of change are key factors for companies investing in Go long-term. Without a clear strategy, code becomes messy, and major architectural decisions are difficult to reverse once implemented.
Characteristics of a Good Structure:
- Consistent: Avoids different approaches in different parts of the codebase.
- Easy to Understand & Navigate: Should make intuitive sense.
- Easy to Change: Loosely coupled components.
- Easy to Test: Should not hinder testing in any way.
- Simple, but No Simpler: Reflects Go's idiomatic philosophy (Einstein quote: "Everything should be made as simple as possible, but not simpler.").
- Reflects Software Design: The structure should mirror how the software actually works. (Rationale: Your code is the most up-to-date documentation of your design, business logic, and language. A structure that aligns with the design is critical for understanding and maintainability.)
Ultimately, a good structure makes a programmer's life easier, working with you, not against you.
Beer Review Service: A Worked Example Specification
[00:06:48]
Zien introduces a simple, realistic example: a beer reviewing service.
User Stories (Functionality):
- Add a beer.
- Add a review for a beer.
- List all beers.
- List all reviews for a given beer.
Technical Requirements:
- Data Storage: Option to store data in-memory or in JSON files. (Analogy: In a real app, this could be a choice between SQL and NoSQL databases.)
- Sample Data: Ability to add sample data to seed the database.
Structure 1: Flat Structure (Initial Approach)
This is the most straightforward way to begin. All files reside in the main package.
[Tactile: Package-tree diagram follows]
. (root directory)
├── data.go (sample data)
├── handlers.go (HTTP handlers)
├── main.go (application entry point)
├── model.go (Beer and Review models)
├── storage.go (Storage interface definition)
├── storage_json.go (JSON file storage implementation)
└── storage_memory.go (In-memory storage implementation)
Tactile Encoding Scheme for Package Tree:
- Directories: Represented by a single raised horizontal line with text label.
- Files: Represented by a shorter raised horizontal line indented relative to its parent directory, with text label.
- Hierarchy: Indentation levels indicate parent-child relationships. A blank line separates main directories.
- Root: The top-most line represents the project root.
Pros:
- Simple: Easy to navigate and reason about for small apps.
- Good Starting Point: Recommended if you don't know where to start.
- No Circular Dependencies: Because everything is in the
mainpackage, circular imports are impossible. (Rationale: The Go compiler prevents packages from importing each other in a cycle. A flat structure bypasses this by keeping everything in one package.)
Cons:
- Global State: Everything is globally accessible and modifiable, preventing encapsulation or "black-boxing".
- Poor Discoverability: File names don't clearly convey the application's purpose. You have to read the code to understand it.
- Scalability Issues: Becomes unmanageable for larger applications (e.g., more than 10-20 business operations).
Structure 2: Grouping by Layer (Layered Architecture/MVC-like)
[00:10:04]
This approach groups code by the functional layer it belongs to, such as presentation (UI), business logic, and external dependencies. It's akin to traditional MVC (Model-View-Controller) patterns.
[Tactile: Package-tree diagram follows]
. (root directory)
├── main.go
├── handlers/
│ └── handlers.go
├── models/
│ └── models.go (Beer, Review structs)
└── storage/
├── storage.go (interface)
├── storage_json.go
└── storage_memory.go
Tactile Encoding Scheme for Package Tree: (Same as above, with added annotations for package relationships if discussed)
Pros:
- Clear Placement: Easy to decide where files go, to a certain extent.
- Discourages Global State: Encourages putting relevant things into the same package.
Cons:
- Shared Variables: Ambiguity arises for variables shared across layers (e.g., where to put
Beerifhandlersandstorageuse it?). - Initialization Dilemma: How and where are dependencies (like storage) initialized and passed around?
- Model Granularity: Should
models.gocontain all model definitions, or separate files per model? - Contextual Ambiguity: A single
Beerstruct might not suit all contexts (e.g., adding a beer doesn't need an ID, but listing one does). (Rationale: A single struct definition forces all contexts to use the same fields, leading to confusion or redundant fields depending on the operation.) - Circular Dependencies: This structure will not compile due to circular dependencies. (Example:
storageimportsmodelsforBeerdefinition, andmodelsmight importstorageto interact with the database.)- [Tactile: Dependency graph follows]
storagepackage───>modelspackagemodelspackage───>storagepackage
Tactile Encoding Scheme for Dependency Graph:
- Packages: Represented by raised rectangles with text labels.
- Dependencies (Arrows): Represented by a raised line with a triangle at one end pointing to the depended-upon package. Direction indicates "imports" or "depends on."
- Circular Dependency: Two arrows forming a closed loop between packages.
- [Tactile: Dependency graph follows]
- Large Files: Model files can grow very large and become unmaintainable.
- Poor Discoverability: Still doesn't tell you much about the app's purpose from package names.
Structure 3: Grouping by Module/Feature
[00:14:21]
This attempts to break the app into logical modules or features, like "beers," "reviews," and "storage."
[Tactile: Package-tree diagram follows]
. (root directory)
├── main.go
├── beers/
│ └── beers.go
├── reviews/
│ └── reviews.go
└── storage/
├── storage.go
├── storage_json.go
└── storage_memory.go
Tactile Encoding Scheme for Package Tree: (Same as above)
Pros:
- Logical Grouping: Components are somewhat logically grouped.
Cons:
- Ambiguous Ownership: Still hard to decide if
reviewsbelong inbeers(as they are beer reviews) or their own package. - Naming Issues (Stutter): Leads to awkward naming like
beer.Beer. (Rationale: Go convention is to avoid repeating the package name in type names. Ifbeerpackage containsBeerstruct, referring tobeer.Beeris redundant and called "stuttering".) - Name Clashes: Can lead to internal name clashes if sub-packages share names with types (e.g.,
storage.JSONvs. a package namedjson). (Example: Ifstoragepackage hasJSONandMemorytypes, you can't have a sub-package also calledjsonwithout renaming.)
Structure 4: Grouping by Context (Domain-Driven Design - DDD)
[00:16:22]
This is a more sophisticated approach, guided by Domain-Driven Design (DDD). DDD focuses on the domain (business logic) first, defining bounded contexts, models, and a ubiquitous language.
Key DDD Concepts:
- Domain: The problem space and business logic.
- Bounded Context: A logical boundary around a particular model. A "user" entity might have different properties and meanings in a "sales" context versus a "customer support" context. (Rationale: DDD helps manage complexity by clearly defining where models are consistent and where they can change independently without affecting other parts of the system.)
- Ubiquitous Language: A shared language used by all stakeholders (developers, sales, support) to refer to concepts in the domain. (Rationale: Ensures everyone is on the same page, reduces inconsistencies (e.g., 'storage' vs. 'database' vs. 'repository'), and facilitates communication across teams.)
DDD Building Blocks:
- Entities: Abstract concepts that have instances and unique identities (e.g.,
Customer,Order,Beer). - Value Objects: Objects that represent a descriptive aspect of the domain and lack conceptual identity (e.g.,
Brewery,ReviewAuthor,Barcodeas part of aBeer). They are immutable. - Aggregates: Combine related entities that need to be treated as a single unit, with a root entity (e.g.,
BeerReviewaggregate containingBeerandReviewentities). - Services: Stateless operations that don't naturally belong to a single entity (e.g.,
BeerAdder,ReviewLister). - Events: Capture "interesting things that happened" in the system, affecting its state (e.g.,
BeerAdded,DuplicateBeerFound). - Repositories: Provide a facade over a backing store, mediating between domain logic and actual storage (e.g.,
BeerRepository,ReviewRepository). (Rationale: Abstracts away storage details, allowing the domain to focus on business logic.)
For the beer service, Zien suggests one main context (beer tasting). Entities: Beer, Review. Value Objects: Brewery, Author. Services: BeerAdder, BeerLister, ReviewLister. Repositories: BeerRepository, ReviewRepository.
Initial Structure based on DDD thinking:
The key here is that package names communicate what they provide (services/actions) rather than what they contain (generic types).
[Tactile: Package-tree diagram follows]
. (root directory)
├── main.go
├── adding/ (Provides 'add beer' functionality)
│ └── service.go
├── listing/ (Provides 'list beers/reviews' functionality)
│ └── service.go
├── reviewing/ (Provides 'add review' functionality)
│ └── service.go
├── beers/ (Beer model definitions for *this context*)
│ └── beer.go
└── reviews/ (Review model definitions for *this context*)
└── review.go
Tactile Encoding Scheme for Package Tree: (Same as above)
Pros:
- Avoids Circular Dependencies: By separating services (adding, listing) that depend on storage, from models (beers, reviews) that are used by storage, the circular link is broken. (
addingandlistingusestorage,storageusesbeersandreviews, butbeersandreviewsdon't directly usestorage). - Context-Specific Models: Each domain package (e.g.,
listing) can have its own definition of aBeerstruct, allowing for different properties based on context (e.g.,addingmight not needBeerID, butlistingdoes).
Cons:
- Sample Data Placement: Where does sample data adding logic go? It's tied into
main, not independent. - New Entry Points: Difficult to add alternative application entry points (e.g., a command-line interface instead of HTTP).
Structure 5: Hexagonal Architecture (Ports and Adapters)
[00:26:54]
The hexagonal architecture (also known as Ports and Adapters) addresses the limitations of the previous DDD-inspired structure. Its core idea is to distinguish between the core domain (business logic) and external dependencies (inputs/outputs). External dependencies are treated as "implementation details".
[Tactile: Hexagonal Architecture Diagram follows]
[Tactile: The diagram should represent a hexagon in the center labeled "Core Domain (Business Logic)". Around the hexagon, but still within an outer boundary, are several "Adapters". Arrows point from the Adapters *inwards* to the Core Domain. Examples of Adapters could be: "HTTP (Input)", "CLI (Input)", "Database (Output)", "Mail Client (Output)".]
Tactile Encoding Scheme for Hexagonal Architecture:
- Core Domain: Represented by a raised hexagon shape with a text label.
- Boundary: A dashed raised line forming a larger hexagon or circle around the core domain.
- Adapters: Raised rectangles or circles outside the core domain, with text labels (e.g., "HTTP", "Database").
- Dependency Flow (Arrows): Raised lines with triangles at one end, always pointing from the outer adapters inward towards the core domain. This is the key rule: outer layers can depend on the domain, but the domain cannot depend on anything outside of it.
Why Hexagonal Architecture?
- Changeability: Allows changing one part of the app (e.g., database type, interface type) without affecting the core business logic. (Rationale: The goal is to isolate business logic from infrastructure concerns. If you swap a MySQL database for a NoSQL database, your core business rules shouldn't need to change.)
- Inputs/Outputs are Equal: Treats inputs (e.g., HTTP requests, CLI commands) and outputs (e.g., database, mail) at the same level, as external interfaces to the core.
How it's Achieved:
- Heavy Use of Interfaces: Interfaces define the contracts between layers. The core domain defines its needs as interfaces, and outer layers implement those interfaces.
- Inversion of Control: The core domain specifies what it needs, and external layers provide the how.
Proposed Structure with Hexagonal Architecture + DDD + Standard Go Layout:
[Tactile: Package-tree diagram follows]
. (root directory)
├── cmd/
│ ├── beer-server/ (Binary for HTTP service)
│ │ └── main.go
│ └── sample-data/ (Binary for adding sample data)
│ └── main.go
├── pkg/
│ ├── adding/ (Domain service: "Add a beer")
│ │ └── service.go
│ ├── listing/ (Domain service: "List beers/reviews")
│ │ └── service.go
│ ├── reviewing/ (Domain service: "Add a review")
│ │ └── service.go
│ ├── http/ (Input adapter: HTTP handlers)
│ │ └── handlers.go
│ └── storage/ (Output adapter: Storage implementations)
│ ├── storage.go (Storage interface defined by domain's needs)
│ ├── json/
│ │ └── json_repo.go (JSON implementation of storage interface)
│ └── memory/
│ └── memory_repo.go (In-memory implementation of storage interface)
└── (other non-Go files: Dockerfile, Makefile, README.md)
Tactile Encoding Scheme for Package Tree: (Same as above. Note the pkg and cmd top-level directories.)
Key Changes & Benefits:
cmd/Directory: Contains binaries.cmd/beer-server/main.go: Main HTTP server.cmd/sample-data/main.go: Separate binary to add sample data, solving the "sample data placement" problem.- Easily add new clients (e.g.,
cmd/cli-client/main.go).
pkg/Directory: Contains all Go packages that are reusable libraries (not binaries). (Rationale: Separates binaries from reusable code, often used for larger projects with many non-Go files in the root.)- Domain Services (Yellow in talk, here implied):
adding,listing,reviewingpackages are in the core domain. - Input/Output Interfaces (Red in talk, here implied):
httpandstoragepackages are adapters.http/handlers.gocontains HTTP endpoint implementations.storage/contains the storage interface (defined by the domain's needs) and separate sub-packages forjsonandmemoryimplementations. This allows easy swapping of storage types.
- Domain Services (Yellow in talk, here implied):
- Improved Discoverability: Package names like
adding,listing,reviewing,beers,reviewsclearly indicate application functionality.
Caution: This structure might be overkill for simple apps. Start simple and evolve. (Rationale: Don't overcomplicate if not necessary. Complexity should match the problem's complexity.)
Implementation Tips
- Start Simple, Evolve: Begin with a flat structure for small projects, then refactor to more complex patterns (like DDD or Hexagonal) as the application grows.
- Focus on "Provides" not "Contains": Name packages based on what functionality they provide or their context (e.g.,
adding,listing), not generic categories (e.g.,utils,common). - Avoid Circular Dependencies: Break dependencies by using interfaces and ensuring inner layers (domain) don't import outer layers (infrastructure).
- [Tactile: Dependency Breaking Diagram follows]
Domain Interface^(Up arrow, depends on)Domain Service|(Down arrow, implements)VInfrastructure Adapter
Tactile Encoding Scheme for Dependency Breaking:
- Layers/Components: Raised rectangles.
- Dependency Arrows: Lines with triangles, indicating the direction of import.
- Key Insight: The interface is defined in the more stable, inner layer (domain), but implemented by the outer, less stable layer (infrastructure). This inverts the typical dependency.
- [Tactile: Dependency Breaking Diagram follows]
- Use Context-Specific Models: Allow different packages or contexts to have their own struct definitions if the semantics differ (e.g.,
adding.Beervs.listing.Beer). - Separate Binaries (
cmd/): For apps with multiple entry points or separate concerns (like a server and a data seeder), put them in distinct directories undercmd/. - Package Reusability (
pkg/): For larger projects, put reusable Go packages underpkg/to clearly separate them from binaries and non-Go root files. - Keep
mainShort: Themainfunction should primarily initialize and kick off the application, delegating complex logic to other packages. - Avoid Global Scope and
init()Functions: Global state makes code harder to reason about and maintain.init()functions run unconditionally and can cause issues with testing or unexpected side effects. (Rationale:init()functions run beforemainand cannot be controlled or turned off, making testing difficult as sample data might be added every time tests run.) - Colocate Tests: Keep test files (
_test.go) alongside the code they test. - Shared Mocks: Mocks can be placed in a shared sub-package (
/mocks) if they are reusable across different tests. - Naming Conventions:
- Avoid stuttering (e.g.,
strings.Readerinstead ofstrings.StringReader). - Avoid generic package names like
utilorcommon, as they become dumping grounds.
- Avoid stuttering (e.g.,
- Be Consistent: Whatever structure you choose, apply it consistently across the project.
- Be Like Water: Be prepared for your structure to evolve and change over time as the application grows and requirements shift. (Rationale: Domain and language evolve, so the code structure must adapt naturally.)
- Experiment and Prototype: Don't be afraid to try different approaches. Look at the standard library for inspiration.
Worked Example: Refactoring a Toy Go HTTP Service
Let's imagine a simple "Hello World" HTTP service that just returns a greeting. We'll refactor it to demonstrate the principles discussed.
Scenario: A service to greet users. Initially, it's a single file. We want to add configurable greetings and perhaps different output methods.
Before: Flat Layout
main.go
// main.go
package main
import (
"fmt"
"log"
"net/http"
)
func greetHandler(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if name == "" {
name = "World"
}
fmt.Fprintf(w, "Hello, %s!\n", name)
}
func main() {
http.HandleFunc("/greet", greetHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}Tactile Annotation: This is a single main package. All logic (HTTP handling, greeting generation) is in one file.
Step-by-Step Refactoring (Towards Hexagonal/DDD)
Refactoring Goal:
- Separate greeting logic from HTTP transport.
- Allow different greeting messages (e.g., "Hi," "Welcome,") to be configurable.
- Prepare for alternative interfaces (e.g., CLI).
Step 1: Extract Core Greeting Logic into a greeting Package (Domain)
We create a pkg/greeting package for the core business logic.
[Tactile: Package-tree diagram follows]
. (root directory)
├── cmd/
│ └── greeter-server/
│ └── main.go
└── pkg/
└── greeting/
└── greeting.go (Core domain logic)
Tactile Annotation: The greeting package is now our core domain. greeter-server is an adapter.
pkg/greeting/greeting.go
// pkg/greeting/greeting.go
package greeting
import "fmt"
// Service defines the core greeting functionality.
type Service struct {
Greeting string
}
// NewService creates a new greeting service.
func NewService(g string) *Service {
return &Service{Greeting: g}
}
// Greet generates a greeting message.
func (s *Service) Greet(name string) string {
if name == "" {
name = "World"
}
return fmt.Sprintf("%s, %s!", s.Greeting, name)
}Tactile Annotation (Code):
package greeting: Defines a new package.type Service struct: A structure for our service, with aGreetingfield.func NewService: A constructor function.func (s *Service) Greet: A method on theServicestruct.- Indentation (raised lines/dots) indicates code blocks and function bodies.
cmd/greeter-server/main.go (Updated)
// cmd/greeter-server/main.go
package main
import (
"fmt"
"log"
"net/http"
"github.com/myuser/myproject/pkg/greeting" // Import our new package
)
func main() {
// Initialize our domain service
greeter := greeting.NewService("Hello") // Configurable greeting
http.HandleFunc("/greet", func(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
message := greeter.Greet(name) // Use the domain service
fmt.Fprintf(w, "%s\n", message)
})
log.Fatal(http.ListenAndServe(":8080", nil))
}Tactile Annotation (Code):
import "github.com/myuser/myproject/pkg/greeting": Shows a dependency on thegreetingpackage.greeter := greeting.NewService("Hello"): Initialization of the domain service.message := greeter.Greet(name): Calling the core domain logic.
How dependencies flow: greeter-server (outer layer/adapter) depends on greeting (inner layer/domain). [Tactile: Arrow from greeter-server to greeting package] (Rationale: The HTTP handler is an "adapter" that interacts with the core "domain" logic. The adapter knows about the domain, but the domain does not know about the HTTP adapter.)
Testing at this stage:
We can now test pkg/greeting/greeting.go in isolation, without any HTTP setup.
pkg/greeting/greeting_test.go
// pkg/greeting/greeting_test.go
package greeting_test
import (
"testing"
"github.com/myuser/myproject/pkg/greeting"
)
func TestGreet(t *testing.T) {
s := greeting.NewService("Hi")
tests := []struct {
name string
expected string
}{
{"Alice", "Hi, Alice!"},
{"", "Hi, World!"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := s.Greet(tt.name); got != tt.expected {
t.Errorf("Greet() = %q, want %q", got, tt.expected)
}
})
}
}Tactile Annotation:
package greeting_test: Indicates a test package forgreeting.func TestGreet(t *testing.T): A standard Go test function.- This test directly exercises the
greeting.Servicewithout needing a running HTTP server.
Step 2: Add a CLI Client (New Adapter)
Now, if we want a command-line interface, it's easy:
[Tactile: Package-tree diagram follows]
. (root directory)
├── cmd/
│ ├── greeter-server/
│ │ └── main.go
│ └── greeter-cli/ (New binary for CLI)
│ └── main.go
└── pkg/
└── greeting/
└── greeting.go
Tactile Annotation: A new binary greeter-cli is added under cmd/.
cmd/greeter-cli/main.go
// cmd/greeter-cli/main.go
package main
import (
"fmt"
"os"
"strings"
"github.com/myuser/myproject/pkg/greeting"
)
func main() {
greeter := greeting.NewService("Greetings") // Different configurable greeting
var name string
if len(os.Args) > 1 {
name = strings.Join(os.Args[1:], " ")
}
message := greeter.Greet(name)
fmt.Println(message)
}Tactile Annotation: This new main.go also imports pkg/greeting and uses its Greet method. [Tactile: Arrow from greeter-cli to greeting package]
Checklist / Decision Guide for Go Project Layout
Use this guide to evaluate your package structure:
- Is this package too big?
- (Consider: If a single package has many unrelated types or functions, or grows excessively large, it might be doing too much.)
- Tactile Check: [Raised dot] Many files for different concerns? [Raised dot] Over 10-20 business operations for a flat structure?
- Is this dependency crossing domain boundaries?
- (Consider: Does an outer layer (e.g., HTTP handler) depend on an inner layer (e.g., core business logic)? This is good. Does an inner layer (e.g., core domain) depend on an outer layer (e.g., database implementation)? This is bad and indicates a circular dependency or leaky abstraction.)
- Tactile Check: [Raised dot] Are domain packages importing infrastructure packages? (🚫) [Raised dot] Are infrastructure packages importing domain packages? (✅)
- Are package names descriptive?
- (Consider: Do they explain what the package provides (e.g.,
adding,listing), or just what it contains generically (e.g.,util,common)?) - Tactile Check: [Raised dot] Do package names use verbs for actions or clear nouns for domain concepts? [Raised dot] Do I understand the package's purpose just from its name?
- (Consider: Do they explain what the package provides (e.g.,
- Am I overcomplicating for a simple problem?
- (Consider: Does the complexity of the structure match the complexity of the problem? Start simple and evolve.)
- Tactile Check: [Raised dot] Is this a small application? [Raised dot] Can I achieve the same goal with fewer packages?
- Is the structure consistent?
- (Consider: Is the same approach applied throughout the project?)
- Tactile Check: [Raised dot] Are all similar components (e.g., HTTP handlers, storage implementations) placed in a consistent manner?
- Can I test components in isolation?
- (Consider: Good structure facilitates easy testing without needing to set up the entire application.)
- Tactile Check: [Raised dot] Can I test my core business logic without spinning up an HTTP server or connecting to a real database?
- Is
main.goconcise and focused on initialization?- (Consider: Does
mainsimply wire things together and kick off the application?) - Tactile Check: [Raised dot] Is
main.goshort? [Raised dot] Does it contain complex business logic or just setup code?
- (Consider: Does
- Am I avoiding
init()functions for critical logic?- (Consider:
init()functions run unconditionally and can complicate testing.) - Tactile Check: [Raised dot] Is any non-trivial logic running in
init()?
- (Consider:
- Am I avoiding global state where possible?
- (Consider: Global state makes it harder to reason about and maintain code.)
- Tactile Check: [Raised dot] Are variables being accessed and modified from many different parts of the application without clear control?
Encoding Go Code Snippets in Braille
For Go code snippets, clarity and consistent formatting are paramount.
- Consistent Indentation: Go uses tabs for indentation. In Braille, a fixed number of spaces (e.g., 2 or 4 Braille cells) should consistently represent one level of indentation. Each indentation level should be clearly distinguishable, perhaps with a slightly larger gap or a specific Braille pattern.
- Tactile Encoding: Use a uniform number of raised dots (e.g., four dots
::::: for one tab) at the beginning of each indented line.
- Tactile Encoding: Use a uniform number of raised dots (e.g., four dots
- Line Numbering (Optional but Recommended): For longer snippets or for reference purposes, line numbers can be helpful. They should be prefixed to the code, separated by a distinct delimiter.
- Tactile Encoding: A Braille number sequence followed by a unique separator (e.g.,
[#]or..) before the code line.
- Tactile Encoding: A Braille number sequence followed by a unique separator (e.g.,
- Keywords: Go keywords (e.g.,
package,import,func,type,struct,interface,if,for,return) should be clearly indicated. Braille formatting for keywords (e.g., capitalizing them if converted to plain text first, or using a specific Braille emphasis) can be helpful.- Tactile Encoding: Keywords can be represented with a distinct texture or a double underline in Braille to make them stand out.
- Comments: Comments should be clearly marked as such, perhaps with a prefix.
- Tactile Encoding: All comments (
//or/* */) should begin with a unique Braille symbol or sequence (e.g.,_C_for single-line comments).
- Tactile Encoding: All comments (
- Brackets and Delimiters:
{,},(,),[,],:,;,,are crucial for Go syntax. Ensure they are distinct.- Tactile Encoding: Standard Braille representations for these symbols are generally sufficient, but ensuring good spacing around them can enhance readability.
- Prefix Notation for Types/Variables: When discussing code conceptually, clearly differentiate between package names, types, and variable names.
- Tactile Encoding:
- Package names:
(PKG)prefix (e.g.,(PKG) main) - Type names:
(TYPE)prefix (e.g.,(TYPE) Service) - Function names:
(FUNC)prefix (e.g.,(FUNC) Greet) - Variable names:
(VAR)prefix (e.g.,(VAR) name) - Interface names:
(INTERFACE)prefix (e.g.,(INTERFACE) Greeter)
- Package names:
- Tactile Encoding:
Example of Braille-friendly Go code rendering:
1: PACKAGE main
2:
3: IMPORT (
4: "fmt"
5: "log"
6: "net/http"
7: )
8:
9: FUNC greetHandler (VAR w http.ResponseWriter, VAR r *http.Request) {
10: (VAR name) := (VAR r).URL.Query().Get("name")
11: IF (VAR name) == "" {
12: (VAR name) = "World"
13: }
14: (FUNC) fmt.Fprintf((VAR w), "Hello, %s!\n", (VAR name))
15: }
Tactile Annotation: The bold keywords and prefixes are examples of how Braille rendering software could highlight these elements to a user.
Q&A / Self-Check Section
Ask yourself these questions about your Go project:
- Domain & Language:
- What is the core domain of my application? Can I clearly define its boundaries?
- What is the "ubiquitous language" that all stakeholders use? Am I using this language consistently in my code?
- What are the key entities, value objects, and services in my domain?
- Package Responsibilities:
- Does each package have a clear, single responsibility that is evident from its name?
- Does the package name describe what it provides (an action or a domain concept), not just what it contains (e.g., "utils")?
- Dependencies:
- Are my dependencies pointing inwards, from outer layers (e.g., HTTP, database implementations) towards my core domain?
- Have I used interfaces to define contracts between layers, allowing for flexibility and testability?
- Are there any circular dependencies? If so, how can I break them by defining interfaces in the dependent package and having the depending package implement them?
- Main Function & Initialization:
- Is my
mainfunction minimal, handling only initialization and orchestration, or is it cluttered with business logic? - Am I using
init()functions for critical logic that might be better placed in explicitly called functions?
- Is my
- Testability:
- Can I easily write unit tests for my core business logic without needing to set up external services (like a database or HTTP server)?
- Are my test files co-located with the code they test?
- Scalability & Flexibility:
- If I needed to add a new client (e.g., a CLI, a gRPC API) or swap a dependency (e.g., change databases), how much code would I need to change?
- Is my structure simple enough for today, but adaptable for future growth?
You can access the original video at: [GopherCon 2018: How Do You Structure Your Go Apps - Kat Zien](http://www.youtube.com/watch?v=oL6JBUk6tj0