Pain points:

  1. The project started out very neat, but as time went by, it gradually became difficult to maintain.
  2. When multiple people are developing the same project, the architecture level is not clear and the wheel is repeated?
  3. Taking over an old project, how to quickly understand the architecture and design, so as to quickly start the requirements?

The benefits of having norms:

  1. Easy for multiple people to develop & understand the same module/project.
  2. Reduce the cost of code communication between team members.
  3. Clear structure & code specification, effectively improve coding efficiency.

Preface:

The first question that comes to mind when reading this book is, “What is clean code?” Here are some of the best quotes from the founders of programmers:

  • The logic is straightforward, making flaws hard to hide.
  • Reduce dependencies and facilitate maintenance.
  • Reasonable stratification, perfect error handling.
  • Only do one thing well. No duplicate code.

Code is a way for teams to communicate

Work communication, not just lark or meeting every day, code is also an important way of communication.

Writing requirements in code is only the first step in a long journey. We express our design ideas in code. If most of the people on our team were able to code according to certain norms and ideas. The cost of communication at work would be much lower. For example, a module one student was in charge of before was taken over by another colleague, or with the expansion of business, several of us jointly developed the same project/module. If our code were structured more or less the same, with clear layers and proper comments, it would save a lot of communication costs.

Therefore, we need to create clean code for the team.

One is to reduce the cost of code communication within the team, and the other is to facilitate the maintenance and iteration of future project requirements.

Leave the camp cleaner than it was when it came

Keep the code clean and the project easier to understand as the requirements iterate.

Sometimes, we maintain some old projects or hand-over projects. The code might not be pretty, the engineering might not be easy to understand.

We are faced with two choices:

  1. refactoring
  2. Optimization iteration

The cost of refactoring is high. You have to understand the original logic before you redesign. The cost is large, the cycle is long, and the short-term effect cannot be seen.

In the case of limited manpower. We usually choose “optimize iteration” first.

At this point, every time we make a new requirement/fix a bug, we “refactor” as small as possible.

With each Merge, the code is cleaner than before and the project becomes more understandable. Then, our project can’t get any worse.

Cleaning doesn’t have to take much effort. Maybe it’s just a more understandable name; Abstract a function to eliminate a bit of duplicate/redundant code; Let’s deal with nested if/else and so on.

One, meaningful naming

Worthy of the name:

Make a name that makes sense. Make it clear. When you look at this variable, you know what object it stores. When you look at this method, you can see what it’s dealing with. By looking at the package name, you can see which module it is responsible for.

Consider the counterexample:

var array []int64
var theList []int64
var num int64
Copy the code

Take a look at the example:

var mrList []*MRInfo
var buildNum int64
Copy the code

Avoid misleading:

Don’t name words that are too long or remote, and don’t use pinyin instead of English. Don’t use confusing letters (letters + numbers). Especially the letters L and O, which look too much like the numbers 1 and 0.

Consider the counterexample:

func getDiZhi(a) string {
   // ..
}

func modifyPassword(password1, password2 string) string {
   // ..
}
Copy the code

Take a look at the example:

func getAddress(a) string {
   // ..
}

func modifyPassword(oldPassword, newPassword string) string {
   // ..
}
Copy the code

A meaningful distinction:

Declare two variables/functions of the same type, distinguished by meaningful names.

Consider the counterexample:

var accountData []*Account
var account []*Account

func Account(id int) *Account {
    // ...
}

func AccountData(id int) *Account {
    // ...
}

Copy the code

Readable and searchable:

Come up with readable, searchable names.

Consider the counterexample:

var ymdhms = "2021-08-04 01:55:55"
var a = 1
Copy the code

Take a look at the example:

var date = "2021-08-04 01:55:55"
var buildNum = 1
Copy the code

Naming conventions (emphasis)

package

  • Packages with the same name are not allowed under the same project.
  • Consists of lowercase letters only. Does not contain uppercase letters and underscores.
  • Be brief and contain contextual information. For example,time,httpAnd so on.
  • It cannot be an obscure common name, or the same name as the standard library. Such as not being able to useutilorstrings.
  • The package name can be used as the base name of the path, and in some cases, different functions need to be split into subpackages. (For example, should useencoding/base64Rather thanencoding_base64orencodingbase64).

The following rules should be met as far as possible in order of precedence:

  • Do not use common variable names as package names.
  • Use singular rather than plural. (Except for keywords, for exampleconsts)
  • Use abbreviations sparingly to ensure understanding.

The file name

  • All file names are lowercase and singular, and can be separated by underscores if necessary.

Functions and methods

Function naming should follow the following principles:

  • Use uppercase letters for exportable functions and lowercase letters for functions used internally.
  • If the function or method is of a judgment type (the return value is mainly bool), the name should start with a judgment verb such as has, is, or can.
// HasPrefix tests whether the string s begins with prefix.
func HasPrefix(s, prefix string) bool {...
Copy the code
  • Functions are named with a hump, cannot be underlined, and cannot repeat the prefix of the package name. For example, usinghttp.ServerRather thanhttp.HTTPServerBecause package names and function names always come in pairs.
// WriteRune appends the UTF-8 encoding of Unicode code point r to b's buffer.
// It returns the length of r and a nil error.
func (b *Builder) WriteRune(r rune) (int, error) {...
Copy the code
  • As a simple rule, you should not use method names like ToString and use String instead.
// String returns the accumulated string.
func (b *Builder) String() string {...
Copy the code
  • Receiver should be short and meaningful
    • Do not use common names in object-oriented programming. For example, don’t useself,this,meAnd so on.
    • It is common to use 1 – or 2-letter abbreviations to represent the original type. For example, of typeClient, you can usec,clAnd so on.
    • Use a common abbreviation for each method of this type. For example, in one of the methodscOn behalf of theClientIt is also used in other methodscInstead of using things likeclThe naming.
func (r *Reader) Len() int {...
Copy the code

constant

  • Constants use the hump form. (Try not to underline)
Const AppVersion = "1.1.1"Copy the code
  • If a constant is an enumerated type, you need to create the corresponding type:
type Scheme string 

 const ( 
    HTTP  Scheme = "http" 
    HTTPS Scheme = "https" 
 )
Copy the code

variable

  • Variable names generally follow the corresponding English expressions or abbreviations.
  • Use hump names and do not use underscores. Capitalization depends on whether external access is required.
  • You don’t have to change the way you write proper nouns. Such as:
{ 
    "API":   true, 
    "ASCII": true, 
    "CPU":   true, 
    "CSS":   true, 
    "DNS":   true, 
    "EOF":   true, 
    "GUID":  true, 
    "HTML":  true, 
    "HTTP":  true, 
    "HTTPS": true, 
    "ID":    true, 
    "IP":    true, 
    "JSON":  true, 
    "LHS":   true, 
    "QPS":   true, 
    "RAM":   true, 
    "RHS":   true, 
    "RPC":   true, 
    "SLA":   true, 
    "SMTP":  true, 
    "SSH":   true, 
    "TLS":   true, 
    "TTL":   true, 
    "UI":    true, 
    "UID":   true, 
    "UUID":  true, 
    "URI":   true, 
    "URL":   true, 
    "UTF8":  true, 
    "VM":    true, 
    "XML":   true, 
    "XSRF":  true, 
    "XSS":   true, 
}

Copy the code

Second, the function

Short,

Shorten the length of each function as much as possible. Abstract when you can.

No function should exceed 50 lines. Even 20 lines is the best. Imagine a function with hundreds or even thousands of lines. How difficult it is to maintain in the back.

Single parameter

Ideally, each function should have zero or one input parameter.

Try not to have more than three inputs. If it does, it is recommended to encapsulate it into a structure.

Just do one thing

The function should only do one thing, do one thing well, do one thing only.

Abstract level

Read/write code from the top down, in order.

Consider the counterexample:

Func UpdatePodUpgradeResult(CTX context. context, req *UpdatePodReq) error {// UpdatePodUpgradeResult(CTX context. Write 40 lines // Update build artifacts, write 20 lines //... More and more code, harder and harder to maintain. return nil }Copy the code

Take a look at the example:

Func UpdatePodUpgradeResult(CTX context.context, Error {// Update component err = updatePodMain(CTX, req) if err! Nil {return err} // Update history err = updatePodHistory(CTX, req) if err! = nil {return err} // Update err = updatePodBuilds(CTX, req) if err! = nil { return err } return nil } func updatePodMain(ctx context.Context, req *UpdatePodReq) error { // ... } func updatePodHistory(ctx context.Context, req *UpdatePodReq) error { // ... } func updatePodBuilds(ctx context.Context, req *UpdatePodReq) error { // ... }Copy the code

Nest if/else as little as possible

Consider the counterexample:

func GetItem(extension string) (Item, error) {
    if refIface, ok := db.ReferenceCache.Get(extension); ok {
        if ref, ok := refIface.(string); ok {
            if itemIface, ok := db.ItemCache.Get(ref); ok {
                if item, ok := itemIface.(Item); ok {
                    if item.Active {
                        return Item, nil
                    } else {
                      return EmptyItem, errors.New("no active item found in cache")
                    }
                } else {
                  return EmptyItem, errors.New("could not cast cache interface to Item")
                }
            } else {
              return EmptyItem, errors.New("extension was not found in cache reference")
            }
        } else {
          return EmptyItem, errors.New("could not cast cache reference interface to Item")
        }
    }
    return EmptyItem, errors.New("reference not found in cache")
}
Copy the code

Take a look at the example:

func GetItem(extension string) (Item, error) { refIface, ok := db.ReferenceCache.Get(extension) if ! ok { return EmptyItem, errors.New("reference not found in cache") } ref, ok := refIface.(string) if ! ok { // return cast error on reference } itemIface, ok := db.ItemCache.Get(ref) if ! ok { // return no item found in cache by reference } item, ok := itemIface.(Item) if ! ok { // return cast error on item interface } if ! item.Active { // return no item active } return Item, nil }Copy the code

Secure Concurrent Processing (SafeGo)

Suggestion: Use SafeGo as far as possible when opening coroutines (recover is available internally and panic stack log is printed)

func SafeGo(ctx context.Context, f func()) { go func() { defer func() { if err := recover(); err ! = nil { content := fmt.Sprintf("Safe Go Capture Panic In Go Groutine\n%s", string(debug.Stack())){ logs.CtxFatal(ctx, content) } } }() f() }() }Copy the code

For Loop Concurrent Processing (Routine Pool)

When a for loop opens a coroutine, the encapsulated Routine Pool is preferred to control concurrency.

Benefits:

  1. Avoid creating too many coroutines, causing the program to crash. (To the service itself)
  2. Control flow speed to prevent avalanche of downstream services. (To downstream services)

Reference code:

type content struct {
	work func(a) error
	end  *struct{}}func work(w func(a) error) content {
	return content{work: w}
}

func end(a) content {
	return content{end: &struct{}{}}
}

// Goroutine routine_pool
type RoutinePool struct {
	capacity uint
	ch       chan content
}

func NewRoutinePool(ctx context.Context, capacity uint) *RoutinePool {
	ch := make(chan content)
	pool := RoutinePool{
		capacity: capacity,
		ch:       ch,
	}

	for i := uint(0); i < capacity; i++ {
		SafeGo(ctx, func(a) {
			for {
				select {
				case cont := <-ch:
					ifcont.end ! =nil {
						return
					}

					ifcont.work ! =nil {
						iferr := cont.work(); err ! =nil {
							LogCtxError(ctx, "run work failed: %v", err)
						}
					}
				}
			}
		})
	}

	return &pool
}

func (pool *RoutinePool) Submit(w func(a) error) {
	pool.ch <- work(w)
}

func (pool *RoutinePool) Shutdown(a) {
	defer close(pool.ch)
	for i := uint(0); i < pool.capacity; i++ {
		pool.ch <- end()
	}
}
Copy the code

Copy passes in the Context of the coroutine

Gin: Just call context.copy ().

3. Comments and formatting

annotation

  • All exportable functions, types, variables, etc., should have comments that start with the function name, type name, and variable name. It is recommended that function comments include descriptions of both parameters and return values.
  • Each comment line should contain no more than 100 characters.
  • Package, function, method, and type annotations are all one complete sentence.
  • There is a specific solution document, leaving a link comment in the corresponding place. This helps you quickly learn about these requirements.

format

This part can be completed as long as we open the Goland configuration.

The recommended configuration

File Watcher opens Go FMT, Go imports:

Configuration can refer to: www.jetbrains.com/help/go/usi…

Vertical format:

Each file from top to bottom code specification.

Try not to exceed 400 lines in a file. (Readability drops if you go beyond that)

  1. Vertical spacing:

A blank line separates the package declaration, import declaration, and each function.

  1. 2. A vertical approach:

The closer the code, the closer the relationship.

  1. Vertical distance:

Variable declaration: as close as possible to where it is used. Local variables, declared at the top of the function. Entity variable, declared at the top of the class.

Correlation function: as close as possible to each other to ensure the order.

First of all, they should all be together. Second, the “called” function should be placed on top of the “called” function.

Conceptually related: functions that do certain kinds of things should be put together.

For example, an interface, which has read/write methods, should be put together

  1. Vertical order:

The “called” function should be placed on top of the “called” function. Establishes a good flow of information from the top down through the source code.

Horizontal format:

Each line of code from left to right code specification.

Try not to exceed 120 words per line of code. (Over 150 words, it’s too much to read on one screen.)

  1. Horizontal spacing and proximity

Enclose Spaces around the operator.

  1. Horizontal alignment
type PodType string

const (
   PodTypeIOS      PodType = "iOS"
   PodTypeAndroid  PodType = "Android"
   PodTypeFlutter  PodType = "Flutter"
)
Copy the code
  1. The indentation

Go-fmt does this for us, just integrate go-FMT.

Objects and data structures

Data is abstracted into objects

Taking component upgrade as an example, the component upgrade process is abstracted into objects. Don’t care about the underlying data structure and implementation.

Analysis: Component upgrade process needs to:

  • ValidateParam (verification parameter)
  • FormatParam
  • SendUpgradeRequest (trigger upgrade)
  • GenerateHistory
  • UpdateHistory
Type mpaasRepoUpgradeHandlerType interface {ValidateParam (CTX context. The context) error / / judge an upgrade request, FormatUpgradeParam(CTX context.context) error SendUpgradeRequest(CTX context. context, history *podHistory) (int, Error) // each Handler sends the upgrade request UpgradeHistory(CTX context.context) *podHistory // generates the UpgradeHistory UpdateHistoryInfo(CTX Context.Context) *podHistory // baseHandler() *podUpgradeBaseHandler // Get baseHandler}Copy the code

Component upgrades are available for iOS, Android, Flutter, Custom (build scripts), RubyGem, etc..

As long as this interface is implemented for any component upgrade, the component upgrade process can be completed.

Data vs. objects

Object: Hides data behind abstraction, exposing methods for manipulating data.

Data: Exposed through data structures.

Process-oriented (direct use of data structures) :

Benefits: New functions can be added without changing existing data structures. Cons: Difficult to add, delete, or modify data structures.

Object-oriented (Abstract) :

Benefits: Easy to add, delete and modify data structures. Cons: Difficult to add new functions, must all class changes.

There is no absolute comparison between the two, and the application of case by case in specific scenarios is needed.

Demeter’s ear law

A module should not know the internal structure of the objects it operates on. Object needs to hide data and expose operations.

5. Error handling

The conventional process

  • Take a look at the counterexample:
package smelly func (store *Store) GetItem(id string) (Item, error) { store.mtx.Lock() defer store.mtx.Unlock() item, ok := store.items[id] if ! ok { return Item{}, errors.New("item could not be found in the store") } return item, nil }Copy the code

Handler to do special handling for special errors:

func GetItemHandler(w http.ReponseWriter, r http.Request) { item, err := smelly.GetItem("123") if err ! = nil { if err.Error() == "item could not be found in the store" { http.Error(w, err.Error(), http.StatusNotFound) return } http.Error(w, errr.Error(), http.StatusInternalServerError) return } json.NewEncoder(w).Encode(item) }Copy the code
  • Let’s look at the example:

Define error types in your package ahead of time.

package clean var ( ErrItemNotFound = errors.New("item could not be found in the store") ) func (store *Store) GetItem(id string) (Item, error) { store.mtx.Lock() defer store.mtx.Unlock() item, ok := store.items[id] if ! ok { return nil, ErrItemNotFound } return item, nil }Copy the code

Handler to do special handling for special errors:

func GetItemHandler(w http.ReponseWriter, r http.Request) { item, err := clean.GetItem("123") if err ! = nil { if errors.Is(err, clean.ErrItemNotFound) { http.Error(w, err.Error(), http.StatusNotFound) return } http.Error(w, err.Error(), http.StatusInternalServerError) return } json.NewEncoder(w).Encode(item) }Copy the code

Benefits: Easy to expand, increase code readability.

Six, boundary

Our systems are microservitized.

Each child service has its own boundaries.

We need to keep our service boundaries as clean as possible.

Boundary neat

The services, libraries, and code we rely on should be under control. Let’s say we rely on a library that we can’t control. If one day he is detected to have a security problem, or a bug. We were passive, and the service needed a major overhaul.

Simply put, it’s better to rely on what we can control than on what we can’t.

So as not to be controlled and lead to rewrite or modification.

Clear hierarchy

Services that belong to the same layer are better off relying only on the lower layer. In theory, you should not rely on services at the same level, much less on services at the upper level.

The architecture diagram of each team/business should be combed out.

Clear responsibilities of modules

In fact, it’s not just services that have a hierarchy between them. We also need to code hierarchically within our services. In addition, the ReadMe of each project should preferably state the general design ideas and architecture for collaborative development.

References:

clean-go-article