Skip to content

New Post about hexagonal architecture in golang #213

@conneroisu

Description

@conneroisu

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 main package, 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 Beer if handlers and storage use it?).
  • Initialization Dilemma: How and where are dependencies (like storage) initialized and passed around?
  • Model Granularity: Should models.go contain all model definitions, or separate files per model?
  • Contextual Ambiguity: A single Beer struct 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: storage imports models for Beer definition, and models might import storage to interact with the database.)
    • [Tactile: Dependency graph follows]
      • storage package ───> models package
      • models package ───> storage package
        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.
  • 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 reviews belong in beers (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. If beer package contains Beer struct, referring to beer.Beer is redundant and called "stuttering".)
  • Name Clashes: Can lead to internal name clashes if sub-packages share names with types (e.g., storage.JSON vs. a package named json). (Example: If storage package has JSON and Memory types, you can't have a sub-package also called json without 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, Barcode as part of a Beer). They are immutable.
  • Aggregates: Combine related entities that need to be treated as a single unit, with a root entity (e.g., BeerReview aggregate containing Beer and Review entities).
  • 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. (adding and listing use storage, storage uses beers and reviews, but beers and reviews don't directly use storage).
  • Context-Specific Models: Each domain package (e.g., listing) can have its own definition of a Beer struct, allowing for different properties based on context (e.g., adding might not need BeerID, but listing does).

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, reviewing packages are in the core domain.
    • Input/Output Interfaces (Red in talk, here implied): http and storage packages are adapters.
      • http/handlers.go contains HTTP endpoint implementations.
      • storage/ contains the storage interface (defined by the domain's needs) and separate sub-packages for json and memory implementations. This allows easy swapping of storage types.
  • Improved Discoverability: Package names like adding, listing, reviewing, beers, reviews clearly 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

  1. 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.
  2. 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).
  3. 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)
      • V
      • Infrastructure 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.
  4. Use Context-Specific Models: Allow different packages or contexts to have their own struct definitions if the semantics differ (e.g., adding.Beer vs. listing.Beer).
  5. Separate Binaries (cmd/): For apps with multiple entry points or separate concerns (like a server and a data seeder), put them in distinct directories under cmd/.
  6. Package Reusability (pkg/): For larger projects, put reusable Go packages under pkg/ to clearly separate them from binaries and non-Go root files.
  7. Keep main Short: The main function should primarily initialize and kick off the application, delegating complex logic to other packages.
  8. 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 before main and cannot be controlled or turned off, making testing difficult as sample data might be added every time tests run.)
  9. Colocate Tests: Keep test files (_test.go) alongside the code they test.
  10. Shared Mocks: Mocks can be placed in a shared sub-package (/mocks) if they are reusable across different tests.
  11. Naming Conventions:
    • Avoid stuttering (e.g., strings.Reader instead of strings.StringReader).
    • Avoid generic package names like util or common, as they become dumping grounds.
  12. Be Consistent: Whatever structure you choose, apply it consistently across the project.
  13. 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.)
  14. 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 a Greeting field.
  • func NewService: A constructor function.
  • func (s *Service) Greet: A method on the Service struct.
  • 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 the greeting package.
  • 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 for greeting.
  • func TestGreet(t *testing.T): A standard Go test function.
  • This test directly exercises the greeting.Service without 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?
  • 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.go concise and focused on initialization?
    • (Consider: Does main simply wire things together and kick off the application?)
    • Tactile Check: [Raised dot] Is main.go short? [Raised dot] Does it contain complex business logic or just setup code?
  • 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()?
  • 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.

  1. 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.
  2. 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.
  3. 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.
  4. 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).
  5. 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.
  6. 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)

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:

  1. 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?
  2. 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")?
  3. 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?
  4. Main Function & Initialization:
    • Is my main function 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?
  5. 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?
  6. 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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions