Go lang: From 0 to Employed
Go lang foundations :: Clean Architecture :: part 3
If you skipped part2 go back here!
Golang Clean Architecture tutorial
Looking to conquer Clean Architecture with Golang? This guide’s got your back. Embark on a coding journey with this compact guide to learn Clean Architecture with Gorilla Mux! You’ll go from project setup to server deployment, learning key concepts such as dependency injection and Clean Architecture. Expect hands-on work with entities, use cases, testing, and building RESTful API endpoints.
By the end, you’ll be equipped with skills to develop, test, and deploy resilient Golang servers. So, ready to transform your coding journey and fast-track your way to Golang proficiency? Let’s get started and make coding magic happen!
Attention: For who is this guide?
This guide is part of a web series intended for individuals who are already proficient in a programming language and are looking to learn GoLang in a simple and fast manner. Therefore, I cover various aspects of the language directly and succinctly, aiming to provide the necessary material for a smooth career transition from other languages to Go. The focus is on supplying ample learning material and support, enabling developers unfamiliar with Go to start working with GoLang as quickly as possible.
Index:
- Introduction to Clean Architecture and Gorilla Mux
- Setting up the development environment
- Creating the project structure
— Folder structure
— Organizing the application in layers - Understanding dependency injection in Clean Architecture
— Separation of concerns
— Inversion of control
— Benefits of dependency injection in Clean Architecture - Defining entities and use cases
— Entities
— Use Cases - Implementing the repository and service
— Repository
— Service - Implementing the presentation layer
— Presenting Gorilla Mux
— Installing Gorilla Mux
— Define routes and HTTP methods
— Create a handler struct
— Create a server Router with the Gorilla Mux router
— Middlewares
— Handling input validation and error handling
— Starting the server - Writing tests for the Golang server
— Unit testing
— Integration testing the API endpoints
— Best practices for testing Clean Architecture applications - Deploying the Golang server
— Overview
— Preparing the application for deployment
— Monitoring and maintaining the application
Feeling lost? Fear not, adventurer! Follow this link for a fresh restart. Your journey starts here!
Writing tests for the Golang server
Testing is an essential part of software development, and writing tests for your Golang server application ensures that it functions as expected and remains maintainable as it evolves. In this section, we’ll discuss how to write tests for your Golang server application built using Clean Architecture principles and Gorilla Mux.
Unit testing
Step 1: Writing unit tests for use cases
Start by writing unit tests for your application’s use cases. Use the testing package in the Go standard library to create tests, and consider using a mocking library, such as github.com/stretchr/testify/mock, to create mock implementations of your repositories or other dependencies.
// domain/post/usecase/post_usecase_test.go
package usecase_test
import (
"testing"
"your_project/domain"
"your_project/domain/mock"
"github.com/stretchr/testify/assert"
)
func TestGetAllPosts(t *testing.T) {
// Set up the mock repository
mockRepo := new(mock.PostRepository)
mockRepo.On("GetAll").Return([]domain.Post{}, nil)
// Set up the use case with the mock repository
useCase := domain.NewPostUseCase(mockRepo)
// Call the use case method
posts, err := useCase.GetAllPosts()
// Assert the expected results
assert.Nil(t, err)
assert.NotNil(t, posts)
mockRepo.AssertExpectations(t)
}
Step 2: Writing unit tests for Repositories
Repositories are responsible for handling data storage and retrieval. To test repositories, you can use the testing package and a mocking library, if necessary, to create mock implementations of external dependencies such as databases.
// domain/post/repository/post_repository_test.go
package repository_test
import (
"testing"
"your_project/internal/domain/post"
"your_project/internal/infrastructure/database/mocks"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func TestPostRepository_GetAll(t *testing.T) {
// Set up the mock database
mockDB := new(mocks.MockDatabase)
// Define what the mock should return when its methods are called
mockDB.On("GetAllPosts").Return([]post.Post{{}, {}}, nil)
// Set up the repository with the mock database
repo := post.NewPostRepository(mockDB)
// Call the repository method
posts, err := repo.GetAll()
// Assert the expected results
assert.Nil(t, err)
assert.Equal(t, 2, len(posts))
// Assert that the method was called on the mock
mockDB.AssertCalled(t, "GetAllPosts")
}
By writing unit tests for entities, use cases, and repositories, you can ensure that the individual components of your Golang server application function as expected. Testing these components separately allows you to isolate issues and catch problems early, making your application more maintainable and less prone to regressions as it evolves.
Step 3: Writing tests for handlers
Next, write tests for your API handlers. You can use the net/http/httptest package in the Go standard library to create a test HTTP server and make requests to your handlers. In your tests, check the response status codes, headers, and bodies to ensure they match the expected results.
// api/post/post_handler_test.go
package post_test
import (
"net/http"
"net/http/httptest"
"testing"
"your_project/api/post"
"your_project/domain"
"your_project/domain/mock"
"github.com/stretchr/testify/assert"
)
func TestGetPosts(t *testing.T) {
// Set up the mock use case
mockUseCase := new(mock.PostUseCase)
mockUseCase.On("GetAllPosts").Return([]domain.Post{}, nil)
// Set up the handler with the mock use case
handler := post.NewPostHandler(mockUseCase)
// Create a test HTTP server and make a request to the handler
req := httptest.NewRequest("GET", "/posts", nil)
rec := httptest.NewRecorder()
handlerFunc := http.HandlerFunc(handler.GetPosts)
handlerFunc.ServeHTTP(rec, req)
// Assert the expected results
assert.Equal(t, http.StatusOK, rec.Code)
// Add more assertions as needed, e.g., checking response headers or body
mockUseCase.AssertExpectations(t)
}
Step 4: Running the tests
Use the go test command to run your tests. You can run tests for a specific package or for your entire project.
# Run tests for a specific package
go test your_project/domain/post/usecase
# Run tests for the entire project
go test ./…
By writing tests for your Golang server application, you can ensure that it functions as expected and is maintainable as it evolves. Testing the individual components of your application, such as use cases and handlers, can help you catch issues early and prevent regressions as you make changes to the codebase. Additionally, a well-tested application is easier to refactor, extend, and scale, as you can verify that existing functionality remains intact as you make modifications.
Integration testing the API endpoints
Integration testing is crucial for ensuring that different components of your application work together as expected. In this section, we’ll discuss how to write integration tests for API endpoints in your Golang server application built using Clean Architecture principles and Gorilla Mux.
Step 1: Set up test environment
Before writing integration tests, set up a test environment that mimics your production environment. This often involves creating a test database with the same schema as your production database and using a configuration that’s as close as possible to your production settings.
// test/test_setup.go
package test
import (
"your_project/config"
"your_project/database"
)
func SetupTestEnvironment() {
config.Load("config_test.yaml")
database.Connect()
}
Step 2: Write integration tests for API endpoints
Use the testing package in the Go standard library and the net/http/httptest package to create a test HTTP server and make requests to your API endpoints. In your tests, check the response status codes, headers, and bodies to ensure they match the expected results. Unlike unit tests, integration tests should involve the actual dependencies, such as the database, to test the interaction between components.
// api/post/post_handler_integration_test.go
package post_test
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"your_project/api/post"
"your_project/domain/post"
"your_project/test"
"github.com/stretchr/testify/assert"
)
func TestIntegration_GetPosts(t *testing.T) {
// Set up the test environment
test.SetupTestEnvironment()
// Set up the handler with real dependencies
repo := post.NewPostRepository(database.GetInstance())
useCase := post.NewPostUseCase(repo)
handler := post.NewPostHandler(useCase)
// Create a test HTTP server and make a request to the handler
req := httptest.NewRequest("GET", "/posts", nil)
rec := httptest.NewRecorder()
handlerFunc := http.HandlerFunc(handler.GetPosts)
handlerFunc.ServeHTTP(rec, req)
// Assert the expected results
assert.Equal(t, http.StatusOK, rec.Code)
var response []post.Post
err := json.Unmarshal(rec.Body.Bytes(), &response)
assert.Nil(t, err)
assert.NotNil(t, response)
// Add more assertions as needed, e.g., checking response headers or body
}
Step 3: Running the integration tests
Use the go test command to run your integration tests. You can run tests for a specific package or for your entire project.
# Run integration tests for a specific package
go test your_project/api/post -tags=integration
# Run all tests, including integration tests, for the entire project
go test -tags=integration ./…
By writing integration tests for your Golang server application, you can ensure that different components work together as expected
Best practices for testing Clean Architecture applications
When testing Clean Architecture applications, it’s essential to follow best practices to ensure your tests are effective, maintainable, and scalable. Here are some best practices to keep in mind when writing tests for your Golang server application built using Clean Architecture principles:
- Isolate components during unit testing: In unit testing, focus on testing individual components in isolation. Use mocking libraries like github.com/stretchr/testify/mock to replace dependencies with mock implementations. This approach ensures that you’re testing the behavior of each component independently and isolating any issues that arise.
- Test the interaction between components in integration testing: Integration testing is crucial for ensuring that different components work together as expected. Unlike unit testing, integration tests should involve the actual dependencies, such as databases, to verify the interaction between components. Make sure you set up a test environment that closely resembles your production environment.
- Keep test cases simple and focused: Aim to keep your test cases simple and focused on a single aspect of a component’s behavior. This approach makes it easier to understand and maintain your tests. Use clear and descriptive names for your test functions to help other developers understand the purpose of each test.
- Use table-driven tests for repetitive testing scenarios: Table-driven tests allow you to define multiple test cases with different input and output values in a single test function. This approach can help reduce code duplication and make your tests easier to maintain. Use subtests with the t.Run() function to run each test case independently and provide a clear test output.
- Test edge cases and error handling: Don’t forget to test edge cases, invalid inputs, and error handling in your application. These tests can help uncover unexpected issues and ensure that your application can handle various scenarios gracefully.
- Ensure code coverage: Monitor your test coverage to ensure that you’re testing all the essential parts of your application. Go provides built-in support for test coverage analysis using the go test -cover command. Aim for a high level of code coverage, but keep in mind that 100% coverage doesn’t guarantee that your application is bug-free.
- Separate unit tests from integration tests: Keep your unit tests and integration tests separate, as they serve different purposes and may have different requirements for execution. You can use build tags (e.g., // +build integration) in your integration test files to distinguish them from unit tests and control which tests are executed using the go test command.
By following these best practices, you can ensure that your tests are effective, maintainable, and scalable. This will help you build a more robust Golang server application using Clean Architecture principles.
Deploying the Golang server
After building and testing your Golang server using Clean Architecture principles and the Gorilla Mux library, the next step is to deploy your application to a production environment. Deployment involves packaging your application and its dependencies, configuring the environment, and setting up the infrastructure to serve your application.
Here’s a high-level overview of the deployment process:
Choose a deployment platform: There are several platforms available for deploying Golang applications, such as cloud providers (e.g., AWS, Google Cloud, Azure), container orchestration systems (e.g., Kubernetes, Docker Swarm), or traditional server environments (e.g., Linux, Windows). Choose a platform that best suits your application’s requirements, budget, and performance expectations.
Package your application: Before deploying your application, you’ll need to package it and its dependencies. You can use the go build command to create a binary executable for your application. For containerized deployments, create a Dockerfile that defines your application’s runtime environment, dependencies, and configuration.
Configure the environment: Configure your production environment with the necessary settings, such as database credentials, API keys, and other sensitive information.
Avoid hardcoding these values in your application code; Instead of hardcoding configuration values, use environment variables or configuration files to manage settings like database connections, API keys, and other sensitive information. Externalizing configuration makes it easier to manage different environments and update settings without rebuilding the application. Use libraries like viper to manage configuration in your Golang application.
Want to learn how to use Viper ? Don’t wait, learn it here!
Set up the infrastructure: Depending on your chosen deployment platform, you may need to set up infrastructure components such as load balancers, databases, and caching systems. Additionally, configure monitoring, logging, and alerting tools to ensure you can track your application’s performance and troubleshoot issues.
Automate the deployment process: Automate your deployment process using continuous integration and continuous deployment (CI/CD) tools like Jenkins, GitLab CI, or GitHub Actions. These tools can help you build, test, and deploy your application automatically when changes are pushed to your repository, reducing manual effort and ensuring a consistent deployment process.
Monitor and maintain your application: After deploying your application, monitor its performance and resource usage to identify potential bottlenecks and issues. Use logging and monitoring tools to collect metrics, analyze logs, and set up alerts to notify you of any issues. Regularly update your application and its dependencies to ensure security, stability, and performance.
By following these steps, you can successfully deploy your Golang server application built using Clean Architecture principles and the Gorilla Mux library. With your application deployed, you can focus on maintaining and improving it to meet the needs of your users.
Ensure that your application handles errors gracefully and logs relevant information for debugging and monitoring purposes. In a production environment, use structured logging to generate logs in a machine-readable format, such as JSON. Libraries like zerolog and zap can help you implement structured logging in your Golang application.
Want to learn how to use structured logging with Zap ? Don’t wait, learn it here!
Preparing the application for deployment
Before deploying your Golang server application, it’s essential to prepare your code and environment for the production setting. Proper preparation helps ensure that your application runs smoothly and securely in the target environment. Here are some steps to prepare your Golang application for deployment:
Compile the application: Use the go build command to compile your application into a single binary. This binary contains your application’s code and dependencies, making it easy to deploy and run on the target platform. For a production build, consider using the -ldflags option to strip debugging information and reduce the binary size:
go build -ldflags="-s -w" -o your_app_name ./cmd/your_app_name
- go build: This is the Go command to compile the Go source files and dependencies into an executable binary.
- -ldflags=”-s -w”: The -ldflags option allows you to pass arguments to the Go linker. Here, two options are passed: -s and -w.
- The -s option tells the linker to remove symbol table and debug information, making the resulting binary smaller.
- The -w option disables the generation of debugging information, also reducing the size of the resulting binary.
- -o your_app_name: The -o option specifies the output filename for the executable binary. Here, it is set to your_app_name.
- ./cmd/your_app_name: This is the package you want to build. The ./ means that it’s in the current directory, and cmd/your_app_name specifies the directory path of the package.
Create a Dockerfile (optional, but highly recommended): If you plan to deploy your application using Docker, create a Dockerfile that defines the application’s runtime environment, dependencies, and configuration. Using a multi-stage build can help you create an optimized and minimal Docker image:
# Build stage
FROM golang:1.17 AS build
WORKDIR /src
COPY .. .
RUN go build -ldflags="-s -w" -o your_app_name ./cmd/your_app_name
# Final stage
FROM gcr.io/distroless/base-debian10
COPY - from=build /src/your_app_name /app/your_app_name
WORKDIR /app
EXPOSE 8080
CMD ["./your_app_name"]
Optimize performance: Profile your application using tools like pprof to identify performance bottlenecks and optimize your code accordingly. Implement caching, connection pooling, and other performance-enhancing techniques as needed to improve your application’s response times and resource usage.
By completing these steps, you’ll ensure that your Golang server application is ready for deployment. Proper preparation helps you avoid potential issues and create a secure, stable, and efficient production environment for your application.
Monitoring and maintaining the application: After deploying your Golang application, it’s crucial to monitor and maintain it to ensure its stability, security, and performance. This section will provide an overview of monitoring and maintaining your Golang application:
Set up logging: Implement structured logging in your application to generate logs in a machine-readable format, such as JSON. Use libraries like zerolog or zap for structured logging in your Golang application. Ensure that your logging configuration is set up to capture relevant information, such as errors, warnings, and other essential events.
Configure monitoring tools: Use monitoring tools to collect metrics and track the performance of your application. Popular monitoring tools for Golang applications include Prometheus, Grafana, and Datadog. These tools help you visualize key performance indicators (KPIs), such as response times, error rates, and resource usage.
Want to learn how to use logs, tracing, and metrics? Coming soon
Set up alerts: Configure alerting systems to notify you when specific thresholds are exceeded or when particular events occur. These alerts can help you proactively identify and address issues before they escalate. Monitoring tools like Prometheus, Grafana, and Datadog often include built-in alerting systems.
Want to learn how to use alerts? Coming soon
Regularly review logs and metrics: Periodically analyze your application’s logs and metrics to identify potential issues, such as performance bottlenecks, resource constraints, or security vulnerabilities. Use this information to optimize your application and address any problems that arise.
Keep dependencies up to date:Regularly update your application’s dependencies to ensure that you’re using the latest, most secure, and best-performing versions. Use tools like Dependabot or Renovate to automate dependency updates.
Implement continuous integration and deployment: Use continuous integration (CI) and continuous deployment (CD) tools to automatically build, test, and deploy your application whenever changes are pushed to the repository. This practice helps you maintain a consistent and stable environment, ensuring that your application is always up to date and functioning correctly.
Want to learn how to use CI/CD? Coming soon
Perform regular backups: Ensure that you regularly back up your application’s data, configuration files, and other critical assets. This practice helps you recover from data loss or corruption and enables you to restore your application to a working state in case of disasters or other unforeseen events.
By monitoring and maintaining your Golang application, you’ll ensure its stability, security, and performance in the target environment. Regularly review logs, metrics, and other data to identify and address potential issues, keeping your application running smoothly and securely.