10 Design Methodology How to Write Elegant Go Projects

10 Design Methodology How to Write Elegant Go Projects #

Hello, I’m Kong Lingfei and today we are going to talk about how to create an elegant Go project.

Go is simple and easy to learn. For most developers, writing runnable code is not a difficult task. However, if you want to truly become a Go programming expert, you need to spend a lot of effort studying Go’s programming philosophy.

In my Go development career, I have seen various code problems, such as: non-standard code, hard to read; poor function reusability, high code duplication; not programming to interfaces, poor code extensibility, code that is difficult to test; low code quality. The reason for these problems is that the developers of these codes rarely spend time studying how to develop an elegant Go project, instead they spend more time blindly developing requirements.

If you have also encountered the aforementioned problems, then it’s time to spend some time studying how to develop an elegant Go project. Only by doing so can you differentiate yourself from the majority of Go developers, establish your core competitiveness in the workplace, and ultimately stand out.

In fact, all the various design standards we have learned before are also aimed at creating an elegant Go project. In this lecture, I have added some additional content to form a methodology for “creating elegant Go projects”. This lecture contains a lot of content, but it is very important. I hope you can spend some time and carefully master it so that you can develop an excellent Go project.

How to Write an Elegant Go Project? #

So, how can we write an elegant Go project? Before answering this question, let’s first address two other questions:

  1. Why a Go project and not just a Go application?
  2. What are the characteristics of an elegant Go project?

Let’s start with the first question. A Go project is a more engineering-focused concept, which not only includes a Go application but also project management and project documentation:

This leads us to the second question. An elegant Go project requires not only an elegant Go application but also elegant project management and documentation. Therefore, based on what we have learned about Go design principles, it is easy to summarize the characteristics of an elegant Go application:

  • Compliant with the Go coding guidelines and best practices;
  • Easy to read, understand, and maintain;
  • Easy to test and extend;
  • High code quality.

After addressing these two questions, let’s get back to the core question of this lesson: How to write an elegant Go project?

In my view, writing an elegant Go project means implementing Go applications, project management, and project documentation using “best practices”. Specifically, this involves writing high-quality Go applications, effectively managing projects, and producing high-quality project documentation.

To help you understand this, I have illustrated the logic in the following diagram:

Next, let’s explore how to implement an elegant Go project based on the Go project design principles we have learned in previous lessons. Let’s start with writing high-quality Go applications.

Writing High-Quality Go Applications #

Based on my R&D experience, writing a high-quality Go application can be summarized into five aspects: code structure, code conventions, code quality, programming philosophy, and software design methods, as shown in the figure below.

Next, let’s discuss these aspects in detail.

Code Structure #

Why do we talk about code structure first? Because an organized code structure is the facade of a project. We can organize the code structure through two means.

The first means is to create a good directory structure. You can review the content of Chapter 06 to learn how to create a good directory structure.

The second means is to choose a good module separation method. Proper module separation allows the modules within a project to have clear responsibilities and achieve low coupling and high cohesion.

So how do we separate modules in Go project development? Currently, there are two commonly used methods in the industry: separating by layers and separating by functionalities.

Firstly, let’s look at separating by layers. The most typical example is the module separation method in the MVC architecture. In the MVC architecture, we split different components of a service into three layers: Model, View, and Controller, based on the access order.

Each layer performs different functions:

  • View: provides user interface for data display.
  • Controller: selects data from the Model layer according to user commands input from the View layer, and performs corresponding operations to produce the final results.
  • Model: the part of the application that handles data logic.

Let’s take a look at a typical directory structure for separating by layers:

$ tree --noreport -L 2 layers
layers
├── controllers
│   ├── billing
│   ├── order
│   └── user
├── models
│   ├── billing.go
│   ├── order.go
│   └── user.go
└── views
    └── layouts

In Go projects, separating by layers will bring many issues. The biggest issue is circular reference: the same functionality may be used in different layers, and these functionalities are scattered across different layers, which easily leads to circular reference.

Therefore, as long as you have a basic understanding of what separating by layers means, in Go projects, I recommend using the separating by functionalities method, which is also the most common method used in Go projects.

So what does separating by functionalities mean? Let me show you an example, and you will understand. For example, in an order management system, we can separate it into three modules: user, order, and billing, based on different functionalities. Each module provides independent functionalities, with a more focused scope:

Here is the directory structure of this order management system:

$ tree pkg
$ tree --noreport -L 2 pkg
pkg
├── billing
├── order
│   └── order.go
└── user

The benefits of splitting modules by function rather than by layer are also easy to understand:

  • Different modules have a single purpose, allowing for high cohesion and low coupling.
  • Because all functions need only be implemented once, clear logic referencing greatly reduces the probability of circular references.

Therefore, many excellent Go projects use module splitting by function, such as Kubernetes, Docker, Helm, and Prometheus.

In addition to this method of organizing the code structure, another effective way to write high-quality Go applications is to follow the Go language code specifications. In my opinion, this is also the easiest way to achieve results.

Code specifications #

So what code specifications should we follow when writing Go applications? In my opinion, there are actually two categories: coding specifications and best practices.

First of all, our code should comply with the Go coding specifications, which is the easiest way to achieve. The Go community has many such specifications for reference, among which the Uber Go Language Coding Specification is quite popular.

Reading these specifications is indeed helpful, but also takes time and effort. Therefore, after referring to many existing specifications and combining my own experience of writing Go code, I have specially prepared a Go coding specification for you in the form of “Special Offering | A Clear and Directly Applicable Go Coding Specification”.

After having a coding specification to refer to, we need to expand it to the team, department, and even company level. Only when everyone participates and complies, will the specifications become meaningful. Actually, we all know that it is not easy for developers to comply with all coding specifications by self-awareness. At this time, we can use static code analysis tools to constrain developers’ behavior.

With static code analysis tools, not only can we ensure that every line of code written by developers complies with the Go coding specifications, but we can also integrate static code analysis into the CI/CD process. This way, code is automatically checked after submission, ensuring that only code that complies with the coding specifications will be merged into the main branch.

There are many static code analysis tools for the Go language, and the most commonly used one currently is golangci-lint, which I highly recommend you to use. I will provide a detailed introduction of this tool to you in Lesson 15.

In addition to following coding specifications, if you want to become a Go programming master, you also need to learn and follow some best practices. “Best practices” are the experience and consensus of the community, which have been accumulated and refined over many years and are in line with the characteristics of the Go language. They can help you develop high-quality code.

Here, I recommend several articles that introduce the best practices of Go language for your reference:

  • Effective Go: Efficient Go programming, written by Golang, contains suggestions for writing Go code, which can also be understood as best practices.
  • Go Code Review Comments: Go best practices written by Golang, complementing Effective Go.
  • Style guideline for Go packages: Contains guidelines for organizing Go packages, naming Go packages, and writing Go package documentation.

Code quality #

After having a well-organized code structure and Go application code that complies with the Go language coding specifications, we also need to ensure that we develop high-quality code through means such as unit testing and code review.

Unit testing is very important. After we develop a piece of code, the first test we execute is the unit test. It ensures that our code is as expected and that any exceptional changes can be promptly detected. Performing unit testing not only requires writing unit test cases, but also ensuring that the code is testable and has a high unit test coverage.

Next, let me introduce how to write testable code.

If we want to test function A and all the code blocks in A can be executed as expected in the unit test environment, then the code blocks in function A are testable. Let’s take a look at the characteristics of a general unit test environment:

  • It may not be able to connect to the database.
  • It may not be able to access third-party services.

Improving Testability with Mocking #

If a function A depends on a database connection or a third-party service, executing unit tests in a testing environment will fail, making the function untestable.

The solution is simple: abstract the dependencies such as the database and third-party services into interfaces. Call the interface methods in the code being tested and pass in mock types during testing. This decouples the dependencies from the specific function being tested. The diagram below illustrates this:

Diagram

To improve the testability of the code and reduce the complexity of unit testing, the following requirements should be met for the function and mock:

  • Minimize dependencies in the function and ensure it only depends on necessary modules. Writing a function with a single responsibility and clear purpose will help reduce dependencies.
  • The dependency modules should be easy to mock.

To help you understand, let’s first look at a piece of untestable code:

package post

import "google.golang.org/grpc"

type Post struct {
    Name    string
    Address string
}

func ListPosts(client *grpc.ClientConn) ([]*Post, error) {
    return client.ListPosts()
}

The ListPosts function in this code is untestable. It calls the client.ListPosts() method, which depends on a gRPC connection. During unit testing, if there’s no gRPC service address configured or network isolation, it won’t be possible to establish a gRPC connection, resulting in the failure of the ListPosts function.

Now, let’s make the code testable by making the following changes:

package main

type Post struct {
    Name    string
    Address string
}

type Service interface {
    ListPosts() ([]*Post, error)
}

func ListPosts(svc Service) ([]*Post, error) {
    return svc.ListPosts()
}

In the modified code, the ListPosts function takes a parameter of type Service interface. As long as we pass an instance that implements the Service interface to ListPosts, the function will run successfully. Therefore, we can create a fake instance in the unit test that doesn’t depend on any third-party service and pass it to ListPosts. Here’s an example of a unit test for the testable code:

package main

import "testing"

type fakeService struct {
}

func NewFakeService() Service {
    return &fakeService{}
}

func (s *fakeService) ListPosts() ([]*Post, error) {
    posts := make([]*Post, 0)
    posts = append(posts, &Post{
        Name:    "colin",
        Address: "Shenzhen",
    })
    posts = append(posts, &Post{
        Name:    "alex",
        Address: "Beijing",
    })
    return posts, nil
}

func TestListPosts(t *testing.T) {
    fake := NewFakeService()
    if _, err := ListPosts(fake); err != nil {
        t.Fatal("list posts failed")
    }
}

Once our code is testable, we can use some tools to mock the required interfaces. Here are a few commonly used mocking tools:

  • golang/mock: This is the official mocking framework provided by Go. It implements mock functionality based on interfaces and integrates well with the built-in testing package. It is the most commonly used mocking tool. The mockgen tool provided by golang/mock generates mock source files corresponding to interfaces.
  • sqlmock: This tool can be used to simulate database connections. Databases are common dependencies in projects, and sqlmock can be used whenever database dependencies are encountered.
  • httpmock: This tool can be used to mock HTTP requests.
  • bouk/monkey: This is the monkey patching tool that allows modification of the implementation of arbitrary functions by replacing function pointers. If golang/mock, sqlmock, and httpmock cannot meet the requirements, monkey patching can be used as the final solution for mocking dependencies in unit tests.

Now, let’s take a look at how to improve the unit test coverage of our project.

Once we have written testable code, the next step is to write enough test cases to increase the unit test coverage of our project. Here are two suggestions to consider:

  • Use the gotests tool to automatically generate unit test code, reducing the effort required to write test cases and freeing you from repetitive labor.
  • Regularly check the unit test coverage. You can use the following methods to check:
$ go test -race -cover -coverprofile=./coverage.out -timeout=10m -short -v ./...
$ go tool cover -func ./coverage.out

The execution result will be as follows:

Coverage Result When improving the unit test coverage of a project, we can start by improving the functions with low unit test coverage, and then check the unit test coverage of the project. If the unit test coverage of the project is still lower than the expected value, we can again improve the coverage of the functions with low unit test coverage, and then check again. We can repeat this process until the unit test coverage of the project is optimized to the expected value.

Note that for some functions that may often change, the coverage of unit tests should reach 100%.

Now that we’ve discussed unit testing, let’s take a look at how to ensure code quality through code review.

Code review can improve code quality, cross-check defects, and promote knowledge sharing within a team. It is a very effective way to ensure code quality. In our project development, it is essential to establish a sustainable and feasible code review mechanism.

However, in my years of development experience, I have found that many teams have not established effective code review mechanisms. These teams acknowledge the benefits of code review mechanisms, but due to difficult adherence to the process, code review slowly becomes formalism and eventually is abandoned. In fact, establishing a code review mechanism is straightforward and mainly involves three points:

  • First, ensure that the code hosting platform we use has code review functionality, such as GitHub and GitLab.
  • Then, establish a set of code review guidelines that specify how code reviews should be conducted.
  • Finally, and most importantly, every time there is a code change, the relevant developers should follow the code review mechanism and develop the habit until it becomes a team culture.

So, to summarize: organizing a reasonable code structure, writing code that conforms to Go code conventions, and ensuring code quality, I believe are the external skills to writing high-quality Go code. What about the internal skills? They are the programming philosophy and software design methods.

Programming Philosophy #

So what does programming philosophy mean? In my opinion, programming philosophy actually means writing code that conforms to the design philosophy of the Go language. The Go language has many design philosophies that have a significant impact on code quality, and I believe two of them are particularly important: interface-oriented programming and “object”-oriented programming.

Let’s first look at interface-oriented programming.

In Go, an interface is a collection of methods. Any type that implements the method set of the interface belongs to that type and is said to implement the interface.

The purpose of interfaces is actually to provide a predefined intermediate layer for modules at different levels. This way, the upstream no longer needs to depend on the specific implementation of the downstream, fully decoupling the upstream and downstream. Many popular Go design patterns are implemented through the idea of interface-oriented programming.

Let’s look at an example of interface-oriented programming. The code below defines a Bird interface, and the types Canary and Crow both implement the Bird interface.

package main

import "fmt"

// Defines a bird class
type Bird interface {
	Fly()
	Type() string
}

// Bird: Canary
type Canary struct {
	Name string
}

func (c *Canary) Fly() {
	fmt.Printf("I am %s, flying with yellow wings\n", c.Name)
}
func (c *Canary) Type() string {
	return c.Name
}

// Bird: Crow
type Crow struct {
	Name string
}
package main

import "fmt"

// 定义一个Bird接口
type Bird interface {
	Fly()
	Type() string
}

// 定义一个Canary结构体实现Bird接口
type Canary struct {
	Name string
}

func (c *Canary) Fly() {
	fmt.Printf("我是%s,我用黄色的翅膀飞\n", c.Name)
}

func (c *Canary) Type() string {
	return c.Name
}

// 定义一个Crow结构体实现Bird接口
type Crow struct {
	Name string
}

func (c *Crow) Fly() {
	fmt.Printf("我是%s,我用黑色的翅膀飞\n", c.Name)
}

func (c *Crow) Type() string {
	return c.Name
}

// 让鸟类飞一下
func LetItFly(bird Bird) {
	fmt.Printf("Let %s Fly!\n", bird.Type())
	bird.Fly()
}

func main() {
	LetItFly(&Canary{"金丝雀"})
	LetItFly(&Crow{"乌鸦"})
}

In this code, because both Crow and Canary implement the Fly and Type methods declared in the Bird interface, it can be said that Crow and Canary implement the Bird interface and belong to the Bird type. In the function call, the Bird type can be passed, and the methods provided by the Bird interface can be called inside the function to decouple the specific implementation of Bird.

Now let’s summarize the advantages of using interfaces:

  • Code extensibility is improved. For example, the same Bird can have different implementations. In development, it is more common to abstract database CRUD operations into interfaces in order to achieve the purpose of using the same code to connect different databases.
  • Upstream and downstream implementations can be decoupled. For example, LetItFly does not need to be concerned with how Bird flies, only needs to call the methods provided by Bird.
  • Code testability is improved. Because interfaces can decouple upstream and downstream implementations, when we need to rely on the code of third-party systems/databases in unit testing, we can use interfaces to decouple the specific implementations and implement fake types.
  • The code is more robust and stable. For example, if you want to change the way Fly works, you only need to change the Fly method of the relevant type, which will completely not affect the LetItFly function.

Therefore, I recommend that you consider using interfaces when developing Go projects, especially when there are multiple possible implementations in certain areas.

Next, let’s take a look at Object-Oriented Programming (OOP).

Object-oriented programming has many advantages, such as making our code easier to maintain, expand, and improving development efficiency, so a high-quality Go application should also adopt object-oriented programming methods when needed. So what does “when needed” mean? It means that when we develop code, if a functionality can be implemented by thinking in a way that is close to daily life and nature, it is the time to consider using object-oriented programming methods.

Go language does not support object-oriented programming, but it can achieve similar effects through some language-level features.

In object-oriented programming, there are several core features: class, instance, abstraction, encapsulation, inheritance, polymorphism, constructor, destructor, method overloading, and this pointer. In Go, similar effects can be achieved through the following ways:

  • Class, abstraction, and encapsulation are implemented through structs.
  • Instance is implemented through struct variables.
  • Inheritance is implemented through composition. Here is an explanation of composition: When one struct is embedded in another struct, it is called composition. For example, if a struct includes an anonymous struct, it is said that this struct is composed of the anonymous struct.
  • Polymorphism is implemented through interfaces.

As for constructor, destructor, method overloading, and this pointer, Go removes these features to maintain language simplicity.

The object-oriented programming methods in Go are shown in the following diagram:

Let’s take a look at how Go implements class, abstraction, encapsulation, inheritance, and polymorphism in object-oriented programming through an example. The code is as follows:

```go
package main

import "fmt"

// Base class: Bird
type Bird struct {
	Type string
}

// Bird class
func (bird *Bird) Class() string {
	return bird.Type
}

// Define a bird class
type Birds interface {
	Name() string
	Class() string
}

// Bird class: Canary
type Canary struct {
	Bird
	name string
}

func (c *Canary) Name() string {
	return c.name
}

// Bird class: Crow
type Crow struct {
	Bird
	name string
}

func (c *Crow) Name() string {
	return c.name
}

func NewCrow(name string) *Crow {
	return &Crow{
		Bird: Bird{
			Type: "Crow",
		},
		name: name,
	}
}

func NewCanary(name string) *Canary {
	return &Canary{
		Bird: Bird{
			Type: "Canary",
		},
		name: name,
	}
}

func BirdInfo(birds Birds) {
	fmt.Printf("I'm %s, I belong to %s bird class!\n", birds.Name(), birds.Class())
}

func main() {
	canary := NewCanary("CanaryA")
	crow := NewCrow("CrowA")
	BirdInfo(canary)
	BirdInfo(crow)
}

Save the above code in a file named oop.go and execute the following code to get the output:

$ go run oop.go
I'm CanaryA, I belong to Canary bird class!
I'm CrowA, I belong to Crow bird class!

In the example above, the Canary and Crow bird classes are defined using the Canary and Crow structs respectively. These structs encapsulate the name property and the Name() method, which represent the name of the bird.

In the Canary and Crow structs, there is an anonymous Bird field. The Bird field is the superclass of the Canary and Crow classes, and they inherit the Class() property and method from the Bird class. In other words, inheritance is achieved through anonymous fields.

In the main() function, a Canary bird class instance is created using NewCanary() and passed to the BirdInfo() function. Similarly, a Crow bird class instance is created using NewCrow() and passed to the BirdInfo() function. In other words, instantiation is achieved through struct variables.

In the BirdInfo() function, the Birds interface type is passed as a parameter and the Name() and Class() methods of the birds object are called. These methods return different names and classes depending on the type of bird. In other words, polymorphism is achieved through interfaces.

Software Design Methods #

Next, let’s continue learning the second set of skills for writing high-quality Go code, which is to follow established software design methods.

There are many excellent software design methods, but two methods are particularly helpful for improving code quality: Design Patterns and SOLID Principles.

In my opinion, Design Patterns can be understood as the best practices that have been developed for specific scenarios in the industry. They are characterized by being applicable to specific situations and are relatively easy to implement. On the other hand, SOLID Principles focus more on design principles, requiring a thorough understanding and more thinking when writing code.

Regarding Design Patterns and SOLID Principles, here’s how I’m going to proceed: In Lesson 11, I will teach you the commonly used design patterns in Go projects. As for SOLID Principles, there are already many high-quality articles available online, so I will briefly explain the principles and recommend an article for you to read.

Let’s start by understanding the different design patterns.

In the software field, a number of excellent design patterns have been developed, with the most popular one being the Gang of Four (GoF) design patterns. The GoF design patterns consist of three categories (Creational Patterns, Structural Patterns, and Behavioral Patterns) and a total of 25 classic design patterns that provide solutions to common software design problems. These 25 design patterns are also applicable to Go projects.

Here, I have summarized these 25 design patterns into one image for you to have a general impression. In Lesson 11, I will provide a detailed introduction to the commonly used design patterns in Go projects.

If design patterns are focused on solving specific scenarios, then SOLID Principles serve as guidelines for designing application code.

SOLID Principles were introduced by Robert C. Martin in the early 21st century. They consist of five basic principles for object-oriented programming and design:

Following SOLID Principles can ensure that the code we design is easy to maintain, extend, and read. SOLID Principles are also applicable to Go program design.

If you would like to learn more about SOLID Principles in detail, you can refer to this article: Introduction to SOLID Principles.

With this, we have completed the section on “Writing High-quality Go Applications”. Next, we will learn how to efficiently manage Go projects and how to write high-quality project documentation. Most of the content here has already been covered in the previous lessons because they are important components of “how to write elegant Go projects”. Therefore, I will give a brief introduction to these topics.

Efficient Project Management #

To have an elegant Go project, efficient project management features are also necessary. So how can we manage our projects efficiently?

Different teams and projects may use different methods to manage projects. In my opinion, there are three important points: establishing an efficient development process, using Makefile to manage projects, and automating project management. We can automate project management by generating code, using tools, and integrating with CI/CD systems. See the diagram below for details:

Efficient Development Process #

The first step in efficient project management is to have an efficient development process. This can improve development efficiency and reduce software maintenance costs. You can recall the knowledge of designing the development process. If your impression is vague, be sure to review the content of Lecture 08, as this part is very important.

Using Makefile to Manage Projects #

In addition to an efficient development process, it is also important to use Makefile to manage projects. Makefile can automate project management through dependencies specified in Makefile. In addition to improving management efficiency, it can also reduce human errors and standardize operations, making the project more standardized.

All operations of the IAM project are completed through Makefile. The Makefile performs the following operations:

 build              Build source code for host platform.
 build.multiarch    Build source code for multiple platforms. See option PLATFORMS.
 image              Build docker images for host arch.
 image.multiarch    Build docker images for multiple platforms. See option PLATFORMS.
 push               Build docker images for host arch and push images to registry.
 push.multiarch     Build docker images for multiple platforms and push images to registry.
 deploy             Deploy updated components to development env.
 clean              Remove all files that are created by building.
 lint               Check syntax and styling of go sources.
 test               Run unit test.
 cover              Run unit test and get test coverage.
 release            Release iam
 format             Gofmt (reformat) package sources (exclude vendor dir if existed).
 verify-copyright   Verify the boilerplate headers for all files.
 add-copyright      Ensures source code files have copyright license headers.
 gen                Generate all necessary files, such as error code files.
 ca                 Generate CA files for all iam components.
 install            Install iam system with all its components.
 swagger            Generate swagger document.
 serve-swagger      Serve swagger spec and docs.
 dependencies       Install necessary dependencies.
 tools              install dependent tools.
 check-updates      Check outdated dependencies of the go projects.
 help               Show this help info.

Code Generation #

The concept of low-code development is becoming increasingly popular. Although low-code has many disadvantages, it does have many advantages, such as:

  • Automatic code generation reduces workload and improves work efficiency.
  • Code with established generation rules is more accurate and standardized compared to manually written code.

Currently, code generation has become a trend. For example, many codes in the Kubernetes project are automatically generated. I think that if you want to create an elegant Go project, you should also consider which parts of the code can be automatically generated. In the IAM project of this course, a large amount of code is automatically generated, and I will provide it here for your reference:

  • Error codes and documentation.
  • Automatically generated doc.go files that were missing.
  • Use the gotests tool to automatically generate unit test cases.
  • Use the Swagger tool to automatically generate Swagger documentation.
  • Use the Mock tool to automatically generate Mock instances for interfaces.

Leveraging Tools #

During the development of a Go project, we should also leverage tools to help us complete some of the work. Using tools can bring many benefits:

  • Freeing up hands and improving work efficiency.
  • Using the determinism of tools to ensure the consistency of execution results. For example, using golangci-lint to check the code can ensure that the code developed by different developers at least follows the code checking rules of golangci-lint.
  • Helps achieve automation by integrating tools into the CI/CD process to trigger automatic execution in the pipeline.

So, what tools can be used in Go projects? Here, I have compiled some useful tools for you:

All these tools can be installed as follows:

$ cd $IAM_ROOT
$ make tools.install

The IAM project uses most of these tools to improve the degree of automation in the entire project and enhance project maintenance efficiency.

Integrating with CI/CD #

When code is merged into the main branch, there should be a CI/CD process to automatically check, compile, and run unit tests on the code. Only code that passes through can be merged into the main branch. Use the CI/CD process to ensure code quality. Popular CI/CD tools include Jenkins, GitLab, Argo, Github Actions, and JenkinsX. In Lecture 51 and Lecture 52, I will provide detailed explanations and hands-on examples of CI/CD principles.

Writing High-Quality Project Documentation #

In the end, an elegant project should also have comprehensive documentation. For example, README.md, installation documents, development documents, usage documents, API interface documents, design documents, etc. These contents are covered in detail in the document specification section of Lesson 04, you can review it for more information.

Summary #

The core purpose of using Go language for project development is to develop an elegant Go project. So how do we develop an elegant Go project? A Go project consists of three main components: Go application, project management, and project documentation. Therefore, developing an elegant Go project actually means writing high-quality Go applications, efficiently managing the project, and writing high-quality project documentation. For each of these, I have provided some implementation methods, which can be seen in the following diagram:

Homework Exercises #

  1. In your work, what other methods do you have to help you develop an elegant Go project?
  2. In your current project, which parts of the code can be abstracted into interfaces? Identify them, and try rewriting this section of code using the philosophy of interface-oriented programming.

Looking forward to seeing your thoughts and answers in the comments. See you in the next lesson!