The author | Yang Chengli (forget hedge) alibaba senior technical experts

Pay attention to the “Alibaba cloud original” public account, reply to Go to view the clear knowledge of the big picture!

How to solve these problems and how to solve these problems, we provide the key technical guide for Go development. This is the third in a four-part series called The Key Technical Guide to Go Development.

Go Development Guide

Interfaces

The type and interface considerations of Go are:

  • Go type systems are not OO in the general sense and do not support virtual functions;
  • Go’s interface is an implicit implementation that is more flexible and easier to adapt and replace;
  • Go supports combination, small interface, combination + small interface;
  • Interface design should consider orthogonality, composition is more conducive to orthogonality.

Type System

Go’s type system is relatively easy to confuse with C++/Java, especially after getting used to the idea of class system and virtual function, it is easy to want to Go this way in Go, but unfortunately it is impossible to Go. Because interface is too simple and not very different from the concepts in C++/Java, this chapter is devoted to analyzing the type system of Go.

Is it possible to call overridden method from parent struct in golang? The code looks like this:

package main

import (
  "fmt"
)

type A struct{}func (a *A) Foo(a) {
  fmt.Println("A.Foo()")}func (a *A) Bar(a) {
  a.Foo()
}

type B struct {
  A
}

func (b *B) Foo(a) {
  fmt.Println("B.Foo()")}func main(a) {
  b := B{A: A{}}
  b.Bar()
}
Copy the code

It is essentially A TemplateMethodPattern, in which A’s Bar calls the virtual function Foo and expects subclasses to override the virtual function Foo, typical C++/Java problem-solving.

Using the TemplateMethodPattern example, consider implementing a cross-platform compiler that provides the user with a function called crossCompile, This calls the two template methods collectSource and compileToTarget:

public abstract class CrossCompiler {
  public final void crossCompile() {
    collectSource();
    compileToTarget();
  }
  //Template methods
  protected abstract void collectSource();
  protected abstract void compileToTarget();
}
Copy the code

C: CrossCompiler use StateMachine

// g++ compiler.cpp -o compiler && ./compiler
#include <stdio.h>

void beforeCompile(a) {
  printf("Before compile\n");
}

void afterCompile(a) {
  printf("After compile\n");
}

void collectSource(bool isIPhone) {
  if (isIPhone) {
    printf("IPhone: Collect source\n");
  } else {
        printf("Android: Collect source\n"); }}void compileToTarget(bool isIPhone) {
  if (isIPhone) {
    printf("IPhone: Compile to target\n");
  } else {
        printf("Android: Compile to target\n"); }}void IDEBuild(bool isIPhone) {
  beforeCompile();

  collectSource(isIPhone);
  compileToTarget(isIPhone);

  afterCompile();
}

int main(int argc, char** argv) {
  IDEBuild(true);
  //IDEBuild(false);
  return 0;
}
Copy the code

The C version uses OOAD thinking and can refer to C: CrossCompiler. The code is as follows:

// g++ compiler.cpp -o compiler && ./compiler
#include <stdio.h>

class CrossCompiler {
public:
  void crossCompile(a) {
    beforeCompile();

    collectSource();
    compileToTarget();

    afterCompile();
  }
private:
  void beforeCompile(a) {
    printf("Before compile\n");
  }
  void afterCompile(a) {
    printf("After compile\n");
  }
// Template methods.
public:
  virtual void collectSource(a) = 0;
  virtual void compileToTarget(a) = 0;
};

class IPhoneCompiler : public CrossCompiler {
public:
  void collectSource(a) {
    printf("IPhone: Collect source\n");
  }
  void compileToTarget(a) {
    printf("IPhone: Compile to target\n"); }};class AndroidCompiler : public CrossCompiler {
public:
  void collectSource(a) {
      printf("Android: Collect source\n");
  }
  void compileToTarget(a) {
      printf("Android: Compile to target\n"); }};void IDEBuild(CrossCompiler* compiler) {
  compiler->crossCompile();
}

int main(int argc, char** argv) {
  IDEBuild(new IPhoneCompiler());
  //IDEBuild(new AndroidCompiler());
  return 0;
}
Copy the code

We can implement this compiler for different platforms, such as Android and iPhone:

public class IPhoneCompiler extends CrossCompiler {
  protected void collectSource() {
    //anything specific to this class
  }
  protected void compileToTarget() {
    //iphone specific compilation
  }
}

public class AndroidCompiler extends CrossCompiler {
  protected void collectSource() {
    //anything specific to this class
  }
  protected void compileToTarget() {
    //android specific compilation
  }
}
Copy the code

It works perfectly in C++/Java, but in Go, using struct nesting is the only way to do it, let iPhone comppiler and AndroidCompiler embed CrossCompiler, see Go: The code for TemplateMethod is as follows:

package main

import (
  "fmt"
)

type CrossCompiler struct{}func (v CrossCompiler) crossCompile(a) {
  v.collectSource()
  v.compileToTarget()
}

func (v CrossCompiler) collectSource(a) {
  fmt.Println("CrossCompiler.collectSource")}func (v CrossCompiler) compileToTarget(a) {
  fmt.Println("CrossCompiler.compileToTarget")}type IPhoneCompiler struct {
  CrossCompiler
}

func (v IPhoneCompiler) collectSource(a) {
  fmt.Println("IPhoneCompiler.collectSource")}func (v IPhoneCompiler) compileToTarget(a) {
  fmt.Println("IPhoneCompiler.compileToTarget")}type AndroidCompiler struct {
  CrossCompiler
}

func (v AndroidCompiler) collectSource(a) {
  fmt.Println("AndroidCompiler.collectSource")}func (v AndroidCompiler) compileToTarget(a) {
  fmt.Println("AndroidCompiler.compileToTarget")}func main(a) {
  iPhone := IPhoneCompiler{}
  iPhone.crossCompile()
}
Copy the code

The results are confusing:

# Expect
IPhoneCompiler.collectSource
IPhoneCompiler.compileToTarget

# Output
CrossCompiler.collectSource
CrossCompiler.compileToTarget
Copy the code

Go does not support class inheritance system and polymorphism, Go is object-oriented but not generally understood that kind of object-oriented, in Lao Tzu’s words, “Tao can tao, very Tao”.

In fact, The Composite Reuse Principle (CRP) is one of The most important object-oriented design principles in OOAD. Favor delegation over inheritance as a reuse mechanism should use composition (proxy) in preference to class inheritance. Class inheritance loses flexibility, and the scope of access is larger than composition; Composition has a high degree of flexibility, and composition uses the interfaces of other objects so that minimal information can be obtained.

How does C++ implement the template method using composition instead of inheritance? Consider having the CrossCompiler use services provided by other classes, or use interfaces, as CrossCompiler relies on ICompiler:

public interface ICompiler {
  //Template methods
  protected abstract void collectSource();
  protected abstract void compileToTarget();
}

public abstract class CrossCompiler {
  public ICompiler compiler;
  public final void crossCompile() { compiler.collectSource(); compiler.compileToTarget(); }}Copy the code

For the C version, please refer to C: CrossCompiler Use Composition, as shown below:

// g++ compiler.cpp -o compiler && ./compiler
#include <stdio.h>

class ICompiler {
// Template methods.
public:
  virtual void collectSource(a) = 0;
  virtual void compileToTarget(a) = 0;
};

class CrossCompiler {
public:
  CrossCompiler(ICompiler* compiler) : c(compiler) {
  }
  void crossCompile(a) {
    beforeCompile();

    c->collectSource();
    c->compileToTarget();

    afterCompile();
  }
private:
  void beforeCompile(a) {
    printf("Before compile\n");
  }
  void afterCompile(a) {
    printf("After compile\n");
  }
  ICompiler* c;
};

class IPhoneCompiler : public ICompiler {
public:
  void collectSource(a) {
    printf("IPhone: Collect source\n");
  }
  void compileToTarget(a) {
    printf("IPhone: Compile to target\n"); }};class AndroidCompiler : public ICompiler {
public:
  void collectSource(a) {
      printf("Android: Collect source\n");
  }
  void compileToTarget(a) {
      printf("Android: Compile to target\n"); }};void IDEBuild(CrossCompiler* compiler) {
  compiler->crossCompile();
}

int main(int argc, char** argv) {
  IDEBuild(new CrossCompiler(new IPhoneCompiler()));
  //IDEBuild(new CrossCompiler(new AndroidCompiler()));
  return 0;
}
Copy the code

We can implement ICompiler for different platforms, such as Android and iPhone. This changes from an inherited class system to a more flexible combination of interfaces and calls to object direct services:

public class IPhoneCompiler implements ICompiler {
  protected void collectSource() {
    //anything specific to this class
  }
  protected void compileToTarget() {
    //iphone specific compilation
  }
}

public class AndroidCompiler implements ICompiler {
  protected void collectSource() {
    //anything specific to this class
  }
  protected void compileToTarget() {
    //android specific compilation
  }
}
Copy the code

In Go, composition and interfaces are recommended, small interfaces, large objects. This will help you get only the information you should get, or not get too much information and functions you don’t need, Clients should not be forced to depend on methods they do not use. — Robert C. Martin, And The bigger The interface, The weaker The abstraction, Rob Pike. For the embodiment of object-oriented principles in Go, please refer to Go: SOLID or the Chinese version of Go: SOLID.

Let’s look at how to implement the previous example using Go, cross-platform Compiler, Go Composition: Compiler, code as follows:

package main

import (
  "fmt"
)

type SourceCollector interface {
  collectSource()
}

type TargetCompiler interface {
  compileToTarget()
}

type CrossCompiler struct {
  collector SourceCollector
  compiler  TargetCompiler
}

func (v CrossCompiler) crossCompile(a) {
  v.collector.collectSource()
  v.compiler.compileToTarget()
}

type IPhoneCompiler struct{}func (v IPhoneCompiler) collectSource(a) {
  fmt.Println("IPhoneCompiler.collectSource")}func (v IPhoneCompiler) compileToTarget(a) {
  fmt.Println("IPhoneCompiler.compileToTarget")}type AndroidCompiler struct{}func (v AndroidCompiler) collectSource(a) {
  fmt.Println("AndroidCompiler.collectSource")}func (v AndroidCompiler) compileToTarget(a) {
  fmt.Println("AndroidCompiler.compileToTarget")}func main(a) {
  iPhone := IPhoneCompiler{}
  compiler := CrossCompiler{iPhone, iPhone}
  compiler.crossCompile()
}
Copy the code

In this scenario, the two template methods are defined as two interfaces, which are used by the CrossCompiler because essentially C++/Java defines its functions as abstract functions, meaning that it does not know how to implement them. However, there is no inheritance relationship between iPhone comppiler and AndroidCompiler, and they implement these two interfaces and are used by CrossCompiler. That is, the relationship between them, instead of being forced binding, becomes a combination.

type SourceCollector interface {
	collectSource()
}

type TargetCompiler interface {
	compileToTarget()
}

type CrossCompiler struct {
	collector SourceCollector
	compiler  TargetCompiler
}

func (v CrossCompiler) crossCompile() {
	v.collector.collectSource()
	v.compiler.compileToTarget()
}
Copy the code

Rob Pike describes the types and interfaces of Go in Go Language: Small and Implicit, on page 29:

  • Objects implicitly satisfy interfaces. A type satisfies an interface simply by implementing its methods. There is no “implements” declaration; interfaces are satisfied implicitly. This implicit implementation of the interface is actually very flexible, and we can Refector an object to an interface, and shrink the dependent interface without changing the code elsewhere. For example, if a functionfoo(f *os.File)At first depend onos.File, but may actually just depend onio.ReaderYou can make UTest easier, so you can change it to UTestfoo(r io.Reader)Nothing needs to be changed, especially if the interface is a new custom interface;
  • In Go, interfaces are usually small: one or two or even zero methods. In Go, interfaces are small, very small, only one or two functions; But the object will be large and will use many interfaces. This approach reuses code in the most flexible way while keeping interfaces valid and minimal, which is interface isolation.

The implicit implementation interface has the advantage that two similar modules implementing the same service can provide the service seamlessly, or even simultaneously. For example, when improving existing modules, such as two different algorithms. Even better, private interfaces created by two modules can communicate with each other if they have the same signature. In fact, the same signature is the same interface, regardless of whether it is private or not. This is very powerful and allows different modules to be upgraded at different times, which is very important for the server that provides the service.

Nothing is more seriously mistaken for inheritance than the Embeding of Go, because it is essentially a composition and not an inheritance.

Embeding can significantly reduce Mocking functions in UTest Mocking functions, such as net.Conn, when Mocking Read and Write are required, can be done by embedding net. LoopBack implements the entire Net.conn interface, so you don’t have to write every interface:

type loopBack struct {
    net.Conn
    buf bytes.Buffer
}

func (c *loopBack) Read(b []byte) (int, error) {
    return c.buf.Read(b)
}

func (c *loopBack) Write(b []byte) (int, error) {
    return c.buf.Write(b)
}
Copy the code

Embeding simply proxies the embedded data and functions all over automatically, essentially using the embedded object as a service. Inner; Outer; Inner; Inner (‘ Inner ‘, ‘Inner’, ‘Inner’, ‘Inner’); Inner (‘ Inner ‘, ‘Inner’, ‘Inner’); For inheritance, it is possible to change Outer’s data by calling Inner, because Outer inherits from Inner, so Outer is Inner, and the two are more closely dependent.

If it’s hard to understand why Embeding is not inheritance, essentially no distinction between inheritance and Composition, look at Composition not inheritance, Go choosing Composition not choosing inheritance is a deliberate decision, and object-oriented inheritance, virtual functions, polymorphism, and class trees are overused. The class inheritance tree needs to be designed in the early stage, and it is often found that the class inheritance tree needs to be changed in the evolution of the system, so we cannot design a perfect class inheritance tree accurately in the early stage. Go’s interface and composition, when the interface changes, only the most direct call layer needs to be changed, and no class subtree needs to be changed.

The designs are nothing like hierarchical, subtype-inherited methods. They are looser (even ad hoc), organic, decoupled, independent, and therefore scalable.

A key advantage of composition over inheritance is orthogonality, which is orthogonal.

Orthogonal

Real water no incense, really cow force do not have to pack. — from the Internet

Software is an art as well as a science. In other words, software is engineering. Science means logic, mathematics, binary, more partial basic theory is the need for mathematics, such as C structured programming is demonstrated, those keywords and logic is enough. In fact, the GC of Go is also mathematically proved, as well as some network transmission algorithms, as well as laying a new field of papers such as Google’s paper. Art means, most of the time, without rigorous argument, there are many different ways, but also need to see their own taste or bias, especially prone to verbal fights and arguments, on the good side, good software or code, can be felt very good.

Because most of the time software development is to rely on experience, especially the domestic spoon-feeding education to cultivate the inexplicable hatred of mathematics (” inexplicable “is mainly long forgotten should not forget all the forgotten), so the emphasis on mathematics in the code will inspire a special contempt and suspicion. And this disdain and suspicion should be based on awe and fear — most of the time blowing math in code is considered pretentiousness. Orthogonal, incidentally, is a mathematical term that’s used in linear algebra (that’s what matrices are) to describe the correlation of two vectors, which in a plane are perpendicularity of two lines. Here’s an example:

Vectors A and B are orthogonal to each other.

Narrator: Neema, two perpendicular lines have a connection to the code, but not to the code. Please keep blowing.

Take a look at Go’s descriptions of Orthogonal correlations, and there may be more:

Composition not inheritance Object-oriented programming provides a powerful insight: that the behavior of data can be generalized independently of the representation of that data. The model works best when the behavior (method set) is fixed, but once you subclass a type and add a method, the behaviors are no longer identical. If instead the set of behaviors is fixed, such as in Go’s statically defined interfaces, the uniformity of behavior enables data and programs to be composed uniformly, orthogonally, and safely.

JSON-RPC: a tale of interfaces In an inheritance-oriented language like Java or C++, the obvious path would be to generalize the RPC class, and create JsonRPC and GobRPC subclasses. However, this approach becomes tricky if you want to make a further generalization orthogonal to that hierarchy.

Orthogonal is actually not unique to Go, refer to Orthogonal Software. In fact, many software designs refer to orthogonality, and OOAD, for example, uses it in many places. Let’s start with a practical example. Java, Python, C#, etc., defines a Thread class, which may contain the following methods to manage threads:

var thread = new Thread(thread_main_function);
thread.Start();
thread.Interrupt();
thread.Join();
thread.Stop();
Copy the code

If you think of Goroutine as a thread for Go, then actually Go does not provide the above methods, but several different mechanisms to manage threads:

  • goKey key to start Goroutine;
  • sync.WaitGroupWait for the thread to exit;
  • chanIt can also be used for synchronization, such as waiting for the Goroutine to start or exit, or passing an exit message to the Goroutine.
  • contextCan also be used to manage goroutine, seeContext.
s := make(chan bool, 0)
q := make(chan bool, 0)
go func() {
    s <- true // goroutine started.
    for {
        select {
        case <-q:
            return
        default:
            // do something.
        }
    }
} ()

<- s // wait for goroutine started.
time.Sleep(10)
q <- true // notify goroutine quit.
Copy the code

Note that this is just an example; in practice it is recommended to use Context to manage goroutine.

If you think of Goroutine as a vector, sync as a vector, and chan as a vector, these vectors are not correlated, which means they’re orthogonal.

In the Orthogonal Software example, objects are stored in TEXT or XML files and the serialization functions of the objects can be written directly:

def read_dictionary(file)
  if File.extname(file) == ".xml"
    # read and return definitions in XML from file
  else
    # read and return definitions in text from file
  end
end
Copy the code

The disadvantages of this include:

  1. Logic code is mixed with serialization code, serialization code is everywhere and very difficult to maintain;
  2. If you want to add a serialization mechanism like serializing objects to the network, it’s hard;
  3. Suppose TEXT supports JSON, or INI?

To improve this example, separate the storage:

class Dictionary
  def self.instance(file)
    if File.extname(file) == ".xml"
      XMLDictionary.new(file)
    else
      TextDictionary.new(file)
    end
  end
end

class TextDictionary < Dictionary
  def write
    # write text to @file using the @definitions hash
  end
  def read
    # read text from @file and populate the @definitions hash
  end
end
Copy the code

If you think of Dictionay as a vector, storage as a vector, and JSON or INI as a vector, they can actually be irrelevant.

For another example, consider jSON-rpc above: A tale of interfaces actually changed the serialized part from *gob.Encoder to interface ServerCodec, and then implemented jsonCodec and gobCodec, So RPC and ServerCodec are orthogonal. The non-orthogonal approach is to inherit the two classes jsonRPC and gobRPC from RPC so that RPC and Codec are coupled and not unrelated.

What is it about Orthogonal irrelevance anyway?

  • In mathematics, two unrelated vectors can be used as the basis of space. For example, the x and y axes on the plane are two vectors. These two unrelated vectors x and y can be combined to form any vector on the plane, and any point on the plane can be represented by X and y. If the vectors are not orthogonal, there are some regions that can’t be represented by these two vectors, there are some points that can’t be represented. This in the interface design is: orthogonal interface, can let users flexibly combine to solve a variety of problems to call the way, unrelated vectors can span the entire vector space; Similarly, if it is not orthogonal, sometimes you find that the functionality you want cannot be implemented through the existing interface, and you have to change the definition of the interface;

  • In the case of Goroutine, we can use Sync or Chan to control the Goroutine in the way we want. Context, for example, is an explicit library of functions provided by interfaces such as chan, timeout and value. These orthogonal elements at the language level can be combined into very diverse and rich libraries. Sometimes we need to wait for Goroutine to start, sometimes we don’t; Sometimes you don’t even need to manage Goroutine, sometimes you need to actively tell Goroutine to quit; Sometimes we need to wait for goroutine to fail;

  • Serialization of TEXT or XML, for example, can completely separate the object logic from the storage, avoid the object logic can be everywhere to store the code, the maintainability can be greatly improved. In addition, the coupling of two vectors can also understand that if multiple vector coupling is difficult to achieve, such as a JSON object serialization to support the comment stored in the network has a problem first, then stored as a TEXT file, and if it is stored as an XML file, an update this complex logic to actually very flexible combination, In essence, it is the combination of multiple vectors of space to express the new vector of space (new function);

  • When objects have features and methods that they shouldn’t have, there are huge maintenance costs. For example, if TEXT and XML mechanisms are coupled, then when maintaining the TEXT protocol and trying to understand the XML protocol, changing the TEXT will cause the XML to hang. Copy(SRC, DST IO.ReadWriter) is a problem because SRC obviously doesn’t use Write and DST doesn’t use Read. Copy(SRC IO.Reader, DST IO.Writer) is valid.

It can be seen that Orthogonal interfaces are a key element in interface design, and interfaces need to be conceptually Orthogonal to provide as many interfaces and functions as possible. For example, IO.Reader, IO.Writer, and IO.Closer are orthogonal, because sometimes we need new vectors to read and write, we can use IO.ReadWriter, which is actually a combination of two interfaces.

How can we implement Orthogonal interfaces? Especially for public libraries, this is critical, and can make the difference between providing a library that works well and one that sucks and doesn’t know how to use. A few suggestions:

  1. Easy to use public library, users can know how to use through the IDE prompts, should not provide multiple different paths to achieve a function, will cause a lot of confusion. Android’s address book, for example, has so many completely different classes that it’s actually very difficult to use;

  2. There must be good documentation. It’s impossible to say Why and How in code. Even the Go standard library is heavily commented, and if a public library has no documentation and comments, it can be very difficult to use and maintain;

  3. Be sure to write Example first, and be sure to provide full UTest coverage. No one has the ability to directly design a reasonable library. Only from the user’s point of view can we know what is reasonable. Example is the user’s point of view. The library has plenty of examples. UTest is also a use, but for internal use, it is also necessary.

Please forgive me if I am not rigorous in mathematics. I am terrible in mathematics.

Modules

First of all, for the latest details on modules, you can run the go help Modules command or check out the long manual go Modules. Additionally, modules are easy to use and cheap to migrate.

Go Module benefits, can refer to Demo:

  1. Code does not have to put GOPATH, can be put in any directory, finally do not have to do soft chain;
  2. Module can still use vendor. If you do not need to update the dependency, you do not need to download the dependency code remotely and do not need to put GOPATH.
  3. If it can be referenced directly in a repository, the package within the module will be automatically identified, again without linking to GOPATH.

Go originally used GOPATH to store dependent packages (projects and code). This GOPATH is a common directory, which can be disastrous if the versions of the dependent libraries are different. In 2016, seven years later, the vendor specification was supported, which meant that dependencies were localized and each project used its own vendor folder. However, this did not solve the problem of conflicts (see the analysis below for details), and instead resulted in a chaos of various package management projects. See PKG Management Tools.

In 2017, eight years after the official vendor package manager dep decided on the solution, it seemed that TheOne was finally doomed. However, in 2018, nine years later, a relatively complete scheme of versioning and VGO was proposed. In this year, Go1.11 supported Modules, and in 2019, Go1.12 and Go1.13 improved a lot of Modules content. When Migrating To Go Modules, Part 1 — Using Go Modules, Part 2 — Migrating To Go Modules, and Part 3 — Publishing Go Modules, when Migrating To Go Modules, migrate To Go Modules. This time it’s really confirmed and confirmed that Go Modules is the final solution.

Why so many technical solutions for GOPATH, Vendor and GoModules? In essence to create jobs, index, Proxy and SUM were created at one time. Hahaha. Of course, it is technically necessary to do this, simply to solve the age-old DLL Hell problem of dependency management and version management. Versioning is just a few numbers, such as 1.2.3, which is actually a very complex problem. I recommend reading Semantic Versioning, assuming that the API is well defined and clear, we use the version number to manage API compatibility; The version number is generally defined as major.minor. PATCH, where a MAJOR change means an incompatible API change.MINOR is a functional change but compatible, and PATCH is compatible with BugFix. Since Go packages are URl-based and have no version number information, the initial guideline for package versioning was that interface compatibility must always be maintained:

If an old package and a new package have the same import path, the new package must be backwards compatible with the old package.

Imagine if all the packages we rely on were interface compatible all the time, there would be no problem and no DLL Hell. Unfortunately, this is not the case. If we have provided packages, we know that it is not possible to provide a permanent interface in the beginning for packages that are continuously maintained and updated, and changing interfaces are incompatible. Even if an interface can be the same, and rely on package, dependence and rely on the package, dependence and rely on rely on bag, reciprocating, requires all interfaces in the world is the same, does not have a version problem, so to say, package management is an extremely difficult to solve, Go took 10 years to determine the final plan it is for this reason, The following example will analyze this problem in detail.

Remark: The standard library also has the risk of interface changes. For example, Context was introduced in Go1.7, which controls the application lifecycle, and many subsequent interfaces have CTX context.Context as the first parameter, such as net.DialContext, which is a function added later. And net.Dial also calls it. Another example is http.request. WithContext, which provides a function that passes the context in the structure, because it is not appropriate to add an additional parameter to each Request function. You can see from the context changes to the standard library interface that there’s some inconsistency, there’s a lot of criticism like context should go away for go 2, that you don’t understand putting context as the first parameter in the standard library, For example, Read(CTX context.Context, etc.

GOPATH & Vendor

Let’s start with GOPATH’s approach. Go introduces external packages, which are urls, first in the environment variable $GOROOT and then in $GOPATH. For example, we use Errors and rely on package github.com/ossrs/go-oryx-lib/errors, as shown below:

package main

import (
  "fmt"
  "github.com/ossrs/go-oryx-lib/errors"
)

func main(a) {
  fmt.Println(errors.New("Hello, playground"))}Copy the code

If we run it directly, we will get an error message as follows:

prog.go:5:2: cannot find package "github.com/ossrs/go-oryx-lib/errors" in any of:
	/usr/local/go/src/github.com/ossrs/go-oryx-lib/errors (from $GOROOT)
	/go/src/github.com/ossrs/go-oryx-lib/errors (from $GOPATH)
Copy the code

You need to download the dependency package go get -d github.com/ossrs/go-oryx-lib/errors and then run it. Download and place in GOPATH:

Mac $ ls -lh $GOPATH/ SRC /github.com/ossrs/go-oryx-lib/errors total 72-RW-r --r-- 1 chengli.ycl staff 1.3k Sep 8 15:35 license-RW-r --r-- 1 Chengli. Ycl staff 2.2k Sep 8 15:35 readme. md-rw-r --r-- 1 chengli. Ycl staff 1.0k Sep 8 15:35 bench_test. go-RW-r --r-- 1 Chengli. ycl staff 6.7k Sep 8 15:35 example_test. go-rw-r --r-- 1 chengli.ycl staff 5.7k Sep 8 15:35 example_test. go-rw-r --r-- 1 chengli. Ycl staff 4.7k Sep 8 15:35 stack.goCopy the code

If the package we depend on also depends on other packages, then Go Get will download all dependent packages to GOPATH. This is downloaded to the public GOPATH, which, as you can imagine, causes several problems:

  1. Downloading dependencies from the Internet every time may not be a problem for the United States, but for China, downloading large projects from GITHUB is a very troublesome problem, and there is no break to continue;
  2. If two projects depend on The GOPATH project, if one update causes problems in the other project. For example, a new project downloads the latest dependency library, which may cause problems for other projects.
  3. Version numbers and upgrades cannot be managed independently, depending on the versions of different packages. For example, project A relies on the 1.0 library, while project B relies on the 2.0 library. Note: this problem is still unsolved if A and B are both libraries, they may be referenced by the same project at the same time. If A and B are the final application, there is no problem. The application can use different versions, which are in their own directories.

In order to solve these problems, vendor is introduced. There is a vendor directory under SRC, and all the dependent libraries are downloaded to this directory. At the same time, there will be a description file to describe the version of the dependency, so that the upgrade of different libraries can be realized. Refer to Vendor and the official package manager DEP. But Vendor does not solve all problems, especially incompatible versions of packages, but only projects or applications that compile binary libraries that a project depends on.

If you do not have the deP tool, you can refer to Installation. Then run the following command to import the dependency to the vendor directory:

dep init && dep ensure
Copy the code

This way the dependent files are placed under vendor and no longer need to be downloaded remotely at compile time:

├ ─ ─ Gopkg. Lock ├ ─ ─ Gopkg. Toml ├ ─ ─ t.g o └ ─ ─ vendor └ ─ ─ github.com └ ─ ─ ossrs └ ─ ─ the go - oryx - lib └ ─ ─ errors ├ ─ ─ errors. Go └ ─ ─ stack.goCopy the code

Remark: Vendor also selects version and has version management, but it only selects one version for each package, which is essentially a localized version of GOPATH. If there is a diamond dependency and conflict, there is no solution, which will be explained below.

What is version conflict?

Let’s look at a problem that GOPATH and Vencor can’t solve, an example of a Semantic Import Versioning problem. Consider the case of diamond dependencies, where users rely on the SDKS of two cloud providers, and they may both rely on common libraries, To form a diamond-shaped dependency, users rely on AWS and Azure and they both rely on OAuth:

If the public library package (OAuth in this case) has the same import path (e.g. github.com/google/oauth), but makes incompatibility changes, releases Oauth-R1 and oauth-R2, and one of the cloud providers updates its dependencies, If the other one is not updated, it causes a conflict, and they rely on different versions:

There is no way to support this in Go, except by adding version semantics to the path of the package, that is, by carrying version information along the path (this is the Go Modules), which has nothing to do with elegance, which is actually the best use experience:

Another option would be to change the package path, which would require the package provider to use a special name for each version, but the user would not be able to tell what these names mean and would not know how to choose which version.

Take a look at the three jobs created by Go Modules, index for indexing, Proxy for proxy caching, and sum for signature validation, and their relationships are described in the Big Picture. Go-get obtains the index of the specified package from index, then downloads the data from proxy, and finally obtains the verification information from sum:

Vgo comprehensive practice

Modules modules modules modules modules modules modules modules modules modules modules modules The first is Using Go Modules, how to use Modules, again Using the above example, the code doesn’t change, just execute the command:

go mod init private.me/app && go run t.go
Copy the code

Modules don’t need to be created under GOPATH, unlike vendor, so this is nice.

$GOPATH/ PKG; $GOPATH/ PKG; $GOPATH/ PKG;

Mac:gogogo chengli.ycl$ go mod init private.me/app && go run t.go go: creating new go.mod: module private.me/app go: Finding github.com/ossrs/go-oryx-lib v0.0.7 GO: Downloading github.com/ossrs/go-oryx-lib v0.0.7 GO: Extracting github.com/ossrs/go-oryx-lib v0.0.7 Hello, Playground Mac:gogogo chengli.ycl$cat go.mod module private.me/app go 1.13 require github.com/ossrs/go-oryx-lib v0.0.7 // Indirect Mac: Gogogo chengli.ycl$cat go.sum github.com/ossrs/go-oryx-lib v0.0.7 H1: k8ml3ZLsjIMoQEdZdWuy8zkU0w fbJSyHvT/s9NyeCc = github.com/ossrs/go-oryx-lib v0.0.7 / go mod h1:i2tH4TZBzAw5h+HwGrNOKvP/nmZgSQz0OEnLLdzcT/8= Mac:gogogo chengli.ycl$ tree$GOPATH/ PKG/Users/winlin/go/PKG ├ ─ ─ mod │ ├ ─ ─ cache │ │ ├ ─ ─ the download │ │ │ ├ ─ ─ github.com │ │ │ │ └ ─ ─ ossrs │ │ │ │ └ ─ ─ Go - oryx - lib │ │ │ │ └ ─ ─ @ v │ │ │ │ ├ ─ ─ the list │ │ │ │ ├ ─ ─ v0.0.7. Info │ │ │ │ ├ ─ ─ v0.0.7. Zip │ │ │ └ ─ ─ sumdb │ │ │ └ ─ ─ Sum.golang.org │ │ │ ├ ─ ─ lookup │ │ │ │ └ ─ ─ github.com │ │ │ │ └ ─ ─ ossrs │ │ │ │ └ ─ ─ [email protected] │ └ ─ ─ at github.com │ └ ─ ─ ossrs │ └ ─ ─ [email protected] │ ├ ─ ─ errors │ │ ├ ─ ─ errors. Go │ │ └ ─ ─ stack. Go └ ─ ─ sumdb └ ─ ─ sum.golang.org └ ─ ─ latestCopy the code

You can manually upgrade a library, namely the go Get library:

Mac: Gogogo chengli.ycl$go get github.com/ossrs/go-oryx-lib go: Finding github.com/ossrs/go-oryx-lib v0.0.8 go: Downloading github.com/ossrs/go-oryx-lib v0.0.8 go: Fully github.com/ossrs/go-oryx-lib v0.0.8 Mac: Gogogo chengli.ycl$cat go.mod module private.me/app go 1.13 require Github.com/ossrs/go-oryx-lib v0.0.8Copy the code

To upgrade a package to a specified version, carry the version number, for example, go get github.com/ossrs/[email protected]. Of course, it can also be downgraded, such as the current v0.0.8, can go get github.com/ossrs/[email protected] to v0.0.7 version. You can also upgrade all dependent packages by executing the go get -u command. To view the dependent packages and versions, run the go list -m all command. To check the versions of the specified package, run the go list -m-versions github.com/ossrs/go-oryx-lib command.

Note: For Minimal Version Selection, see Minimal Version Selection.

If more than one version of a package is relied on, the highest version of the package will be selected, for example:

  • If A depends on V1.0.1, b depends on v1.2.3, and the program depends on A and B, then v1.2.3 is used.
  • If a depends on V1.0.1, d depends on v0.0.7, and the program depends on A and D, then v1.0.1 is ultimately used, i.e. v1 is considered compatible with V0.

Rm -f go. Mod && go mod init private.me/app && go run. Always select the highest version of the large version (that is, the smallest version that meets the requirements) :

package main

import (
	"fmt"
	"github.com/winlinvip/mod_ref_a" / / 1.0.1
	"github.com/winlinvip/mod_ref_b" / / 1.2.3
	"github.com/winlinvip/mod_ref_c" / / 1.0.3
	"github.com/winlinvip/mod_ref_d" / / 0.0.7
)

func main(a) {
	fmt.Println("Hello",
		mod_ref_a.Version(),
		mod_ref_b.Version(),
		mod_ref_c.Version(),
		mod_ref_d.Version(),
	)
}
Copy the code

If the package needs to be upgraded to a larger version, you need to add the version to the path, including its own path in go.mod, depending on the package’s go.mod, depending on its code, such as the following example, using both v1 and v2 versions (only one can be used) :

package main

import (
	"fmt"
	"github.com/winlinvip/mod_major_releases"
	v2 "github.com/winlinvip/mod_major_releases/v2"
)

func main(a) {
	fmt.Println("Hello",
		mod_major_releases.Version(),
		v2.Version2(),
	)
}
Copy the code

After running the program, you can see that two packages have been imported into go.mod:

Module private.me/app go 1.13 require (github.com/winlinvip/mod_major_releases v1.0.1 Github.com/winlinvip/mod_major_releases/v2 v2.0.3)Copy the code

Remark: If a specified version of V2 needs to be updated, the path must also carry v2, that is, all paths of v2s must carry v2, for example, go get github.com/winlinvip/mod_major_releases/[email protected].

The library provides the same for large releases, and uses mod_major_releases/v2 to basically do the following:

  1. I’m going to create a new branch of V2,git checkout -b v2, such asGithub.com/winlinvip/m…
  2. Modify the description of go.mod, the path must have v2, for examplemodule github.com/winlinvip/mod_major_releases/v2;
  3. Tag v2 after submission, for exampleGit tag v2.0.0Both branches and tags are committed to Git.

Go.mod is updated as follows:

The module github.com/winlinvip/mod_major_releases/v2 go 1.13Copy the code

The code is updated as follows. Since it is a large version, the function name has been changed:

package mod_major_releases

func Version2(a) string {
	return "MMV / 2.0.3"
}
Copy the code

Note: Modules: v2 for more information, and Russ Cox: From Repository to Modules introduce two methods, the most common being the branch method above, and the folder method.

Go Modules in particular:

  • For a public package, if the package described in go.mod is not the same as the public path, for example, go.mod is private.me/app and published to github.com/winlinvip/app, Of course, there will be errors when importing this package for other projects. For libraries, that is, packages that you want others to rely on, the path described by go.mod and the path to release, as well as the package name, should be the same;

  • If a package has not been released, the latest commit and date are taken in the format of v0.0.0-date-commit, for example, v0.0.0-20191028070444-45532e158b41. See Pseudo Versions. The version number can start with v0.0.x, such as v0.0.1 or v0.0.3 or v0.1.0 or v1.0.1. There is no mandatory requirement to be a 1.0 release;

  • Mod replace does not work on submodules, but only at the top level at which the binary was compiled, as defined in the go.mod that eventually generated the binary. The official instructions are to control dependencies when the binary is finally generated. For example, wants to github.com/pkg/errors rewritten as github.com/winlinvip/errors this package, the correct approach reference branch replace_errors; If replace is not in the main module (top level), refer to replace_in_submodule. Replace is only defined in the submodule but is ignored. Replace_errors is valid if replace is used in the main module, and replace_DEPS_of_submodule is also valid if the main module relies on submodule fast dependencies. However, it can also be replaced in sub-module express, which can be a confusing place to anticipate. When upgrading Go1.13 Errors, the Errors of Go1.13 support the Unwrap interface. When upgrading Go1.13 Errors, when upgrading Go1.13, the Errors of Go1.13 support the Unwrap interface. PKG /errors does not support Go1.13. The author suggests forking PKG /errors. So you can use go mod replace to replace the fork URL with PKG /errors;

  • Go get does not update every library to the latest version. For example, the library github.com/winlinvip/mod_minor_versions has two versions of V1.0.1 and v1.1.2, and currently relies on v1.1.2. If the library is updated to v1.2.3, using go get -u immediately will not update to v1.2.3, nor will going to go get -u github.com/winlinvip/mod_minor_versions. This version will not be used unless you explicitly update go get github.com/winlinvip/[email protected], which will take some time to update;

  • For larger versions such as V2, it must be described by go.mod. Direct references can also be used such as go get github.com/winlinvip/[email protected], which indicates v2.0.0+incompatible, V2.0.0 tag is used as v0 and v1 by default, but v2.0.0 tag is used as v1 by default. If you don’t use go.mod, the tag will be interpreted as v1, and there will be compatibility issues.

  • For example, go get github.com/winlinvip/mod_major_releases/[email protected]. If there is no v2 in the path, an error message will be displayed indicating that the update cannot be performed. For example, go get github.com/winlinvip/[email protected], the error message is invalid version: module contains a go.mod file, so major version must be compatible: Should be v0 or v1, so mod_major_releases is going to be v0 or v1, but then it’s going to be @v2 so it doesn’t match and can’t be updated.

  • As with the above problem, if in go.mod there is no version in the larger version path, such as require github.com/winlinvip/mod_major_releases v2.0.3, Module contains a go.mod file, so major version must be compatible: Should be v0 or v1, this is a bit ambiguous because the package definition of go.mod is v2. This error means that the place require requires v0 or v1, when in fact the version is V2.0.3. This is the same thing as manually requesting updates to go get github.com/winlinvip/[email protected];

  • Note that the three main positions have cache, such as [email protected] go.mod description error, should be V5, not V3. If you get the version go get github.com/winlinvip/mod_major_error/v5, Error: v5.0.0 does not contain package github.com/winlinvip/mod_major_error/v5, but does not contain package github.com/winlinvip/mod_major_error/v5 Because index and GoProxy cache this version of information. The solution version is to upgrade a version of V5.0.1, directly obtain this version can be, such as go get github.com/winlinvip/mod_major_error/[email protected], so that there is no problem. See Semantic versions and modules for details;

  • Same problem as above, if there is a go get request when the version is not released, it will not be able to get the version after it is released. For example, if github.com/winlinvip/mod_major_error does not type version v3.0.1, please go get github.com/winlinvip/mod_major_error/[email protected], You will be prompted that this version is not available. If I put this tag, even if I have this tag, Also will be prompted to 401 find reading https://sum.golang.org/lookup/github.com/winlinvip/mod_major_error/[email protected]: 410 Gone. You can only get it by updating the version and typing a new tag such as V3.0.2.

To sum up:

  • GOPATH, since the default is $HOME/go, works very well. Dependent packages are cached in this public place, as long as the project is small, it is completely straightforward and very useful. It is estimated that GOPATH may be used for a long time. After all, habits are the most terrible thing. Habits are the ones that live the longest, so habits become a way of life.

  • Vendor cache dependencies are local to the project, which solves a lot of problems. Better than GOPATH, dependencies can be updated regularly. In most projects, dependencies need to be updated when necessary, rather than fetching the latest code every time they are compiled. Therefore, vendor is very practical. If you can keep relatively restrained, you should not rely on one package just because you want to use one function. As a result, this package depends on ten, which in turn depend on hundreds.

  • Vgo /modules, no difference in code usage; During the version update, for example, it is clear that the package v2 needs to be imported, the import URL will be different. The code cache uses proxy to download, cached in GOPATH’s PKG, because there is version information, so there is no conflict; It will be safer because sum is there. It’s more flexible because you have index and proxy.

How to migrate seamlessly?

How to migrate existing projects from GOPATH and Vendor to Modules? Migrating to Go Modules, the official migration guide, explains that a project can have three states:

  • A completely new project that has not yet started. So just use modules as above;
  • Existing projects use other dependency management, i.e., vendor, such as DEP or Glide, etc. The go mod converts existing formats to modules. See here for supported formats. Modules will continue to support vendor, as described below.
  • The existing project does not use any dependency management, known as GOPATH. Note that the package path for Go mod init needs to be the same as the exported one, especially the one supported by Go1.4import comment, may not be the same as the path of the warehouse, such as the warehouse inhttps://go.googlesource.com/lint, and the package path isgolang.org/x/lint.

Note: special attention if it is the library support v2 and above version, the path must be need to include the v2, such as github.com/russross/blackfriday/v2. And the need to update the package reference v2 library, more painful, but fortunately this situation is rare.

Let’s take a look at an example using GOPATH. Let’s create a new test package and provide it as GOPATH. See github.com/winlinvip/m… , depending on github.com/pkg/errors, rsc. IO /quote and github.com/gorilla/web… .

Let’s look at another vendor example. Convert this GOPATH project into vendor project, see github.com/winlinvip/m… Dep init = deP init

Chengli. Ycl $dep status PROJECT CONSTRAINT VERSION REVISION LATEST PKGS, informs github.com/gorilla/websocket ^ 1.4.1 v1.4.1 C3e18be v1.4.1 1 github.com/pkg/errors ^0.8.1 v0.8.1 ba968bf v0.8.1 1 golang.org/x/text v0.3.2 v0.3.2 342b2E1 v0.3.2 6 IO /quote ^3.1.0 v3.1.0 0406d72 v3.1.0 1 rsc. IO /sampler v1.99.99 v1.99.99 732a3c4 v1.99.99 1Copy the code

Next, switch to the Modules package and make a copy of github.com/winlinvip/m… The code (copied here to demonstrate the difference, direct conversion is also available) becomes github.com/winlinvip/m… Run the go mod init github.com/winlinvip/mod_gopath_vgo && go test./… Git add. &&git commit -am “Migrate to vgo” &&git tag v1.0.1 &&git push origin v1.0.1:

Mac:mod_gopath_vgo chengli.ycl$cat go.mod module github.com/winlinvip/mod_gopath_vgo go 1.13 require ( Github.com/gorilla/websocket v1.4.1 github.com/pkg/errors v0.8.1 RSC. IO/quote v1.5.2)Copy the code

Depd vendor’s project is the same, first copy github.com/winlinvip/m… As github.com/winlinvip/m… Run the go mod init github.com/winlinvip/mod_vendor_vgo && go test./… Git add. &&git commit -am “Migrate to vgo” &&git tag v1.0.3 &&git push origin v1.0.3:

1.13 the require module github.com/winlinvip/mod_vendor_vgo go (github.com/gorilla/websocket v1.4.1 github.com/pkg/errors V0.8.1 golang.org/x/text v0.3.2 // Indirect rsc. IO /quote v1.5.2 rsc. IO /sampler v1.0.99 //Copy the code

Then you can reference it in other projects:

package main

import (
	"fmt"
	"github.com/winlinvip/mod_gopath"
	"github.com/winlinvip/mod_gopath/core"
	"github.com/winlinvip/mod_vendor"
	vcore "github.com/winlinvip/mod_vendor/core"
	"github.com/winlinvip/mod_gopath_vgo"
	core_vgo "github.com/winlinvip/mod_gopath_vgo/core"
	"github.com/winlinvip/mod_vendor_vgo"
	vcore_vgo "github.com/winlinvip/mod_vendor_vgo/core"
)

func main(a) {
	fmt.Println("mod_gopath is", mod_gopath.Version(), core.Hello(), core.New("gopath"))
	fmt.Println("mod_vendor is", mod_vendor.Version(), vcore.Hello(), vcore.New("vendor"))
	fmt.Println("mod_gopath_vgo is", mod_gopath_vgo.Version(), core_vgo.Hello(), core_vgo.New("vgo(gopath)"))
	fmt.Println("mod_vendor_vgo is", mod_vendor_vgo.Version(), vcore_vgo.Hello(), vcore_vgo.New("vgo(vendor)"))}Copy the code

Note: For private projects, you may not be able to index validation with three major items, so you can set GOPRIVATE to disable validation, see Module Configuration for Non Public Modules.

vgo with vendor

Vendor is not impossible to use, you can use modules with Vendor, see How do I use vendoring with modules? Is vendoring going away? In fact, vendor does not die. There was a detailed discussion in the Go community. Vgo & Vendoring decided to support vendorin Modules. There are several steps to enabling Vendor in Modules:

  1. $GOPATH/ PKG = $GOPATH/ PKG = $GOPATH/ PKG = $GOPATH/ PKG Reference github.com/winlinvip/m… ;

  2. Go mod vendor, this step does, is to put modules files into the vendor. Of course, since go.mod also exists, and of course knows the version information of these files, it doesn’t cause any problems, just creates a new vendor directory. This looks like normal modules and vendor to others, and has no effect at all. Reference github.com/winlinvip/m… ;

  3. If you go build-mod =vendor, you will ignore the vendor directory by default. If you add this parameter, the code will be loaded from the vendor directory (if you delete $GOPATH/ PKG, the code will not be downloaded). Go test -mod=vendor./… Or go run -mod=vendor.

To call this package, use modules to download dependencies, such as go mod init private.me/app && go run t.co:

package main

import (
	"fmt"
	"github.com/winlinvip/mod_vendor_vgo"
	vcore_vgo "github.com/winlinvip/mod_vendor_vgo/core"
	"github.com/winlinvip/mod_vgo_with_vendor"
	vvgo_core "github.com/winlinvip/mod_vgo_with_vendor/core"
)

func main(a) {
	fmt.Println("mod_vendor_vgo is", mod_vendor_vgo.Version(), vcore_vgo.Hello(), vcore_vgo.New("vgo(vendor)"))
	fmt.Println("mod_vgo_with_vendor is", mod_vgo_with_vendor.Version(), vvgo_core.Hello(), vvgo_core.New("vgo with vendor"))}Copy the code

Go mod vendor && go run -mod=vendor t.go. If a new dependent package needs to be imported, you need to use Modules to import it and then go mod vendor to copy it to vendor. In a word, modules with Vendor is a way of putting all dependencies under vendor when you finally submit code.

Note: Ides like Goland have Preferences /Go /Go Modules(vgo) /Vendoring mode, which is resolved from the project vendor directory, not from the global cache. If you do not need to import a new package, you can enable vendor mode by default. Run the go env -w GOFLAGS=’-mod=vendor’ command.

Concurrency&Control

Concurrency is a fundamental problem for servers, and concurrency control is of course a fundamental problem. Go doesn’t eliminate this problem, it just simplifies it.

Concurrency

Eighteen years ago, in 1999, gigabit network cards were a new thing. It was worth investigating when gigabit bandwidth supported only 10K clients. After all, Nginx only came out in 2009, and before that, people had been playing around with HTTP servers in the kernel. The server world is also discussing how to solve the C10K problem. Reading this article feels like entering the workshop of a busy server factory, with thousands of intricate cables interwoven together, and even the thundering Herd, whose legends, like ancient werewolves, can be heard occasionally even in the 21st century. All we’re talking about right now is how to support C10M, which is ten million level concurrency.

Concurrency, no doubt, is an unavoidable topic in the server field and is the basic ability of the server software engineer. Concurrency is definitely one of Go’s biggest strengths. If you had to pick one of Go’s great features, it would be concurrency and engineering. If you had to pick one, it would be concurrency support. Large scale software, or cloud computing, is largely server programming, and servers deal with a few basic problems: Concurrency, clustering, disaster recovery, compatibility, and o&M are all problems that can be improved by Go’s concurrency feature, one of the Essential complexities in the server space, according to Man-month Myth. Go’s concurrency was crucial to its rapid dominance of the cloud computing market.

Using the concept of Essential Complexity in The Myth of the Man-month, the Complexity of concurrency is clear. Even if you haven’t read this book, you must have heard that there is no silver bullet in software development. To maintain the “conceptual integrity” of software, Brooks, as a double expert of hardware and software and an outstanding educator, has always been active in the computer arena and made great contributions in many fields of computer technology. Led the development of the IBM System/360 and IBM OS/360 in 1964 (age 33), won the Von Neumann Prize in 1993 (age 62) and the Turing Prize in 1999 (age 68), He was awarded the IEEE Virtual Reality Career Award (2010) for Virtual Reality (VR) in 2010 (age 79).

Few works in software have been as influential and enduring as the Myth of the Man-Month. Dr. Brooks provides insightful insights into managing complex projects, with many thought-provoking ideas and a wealth of software engineering practice. The book is based on Dr. Brooks’ experience in project management in the IBM System/360 family and OS/360, which is a model of software development project management. As soon as the original English version of the book was published, it caused a strong response from the industry, and was later translated into German, French, Japanese, Russian, Chinese, Korean and other languages, selling millions of copies around the world. Established its classic position in the industry.

Brooks is the person I admire most. He has both theory and practice, understands hardware and software, is committed to large-scale software (before cloud computing) system, has enough foresight (as long as 10 or even 20 years), and keeps working tirelessly. He strongly recommends software engineers to read the Myth of man-month.

Back to our discussion of Concurrency. To understand Concurrency, you have to start by understanding the Concurrency problem itself, and the Concurrency model. In 2012, I learned the concurrency processing mechanism EDSM(Event-driven State Machine Architecture) of NGINX, which is famous for its high concurrency, when I was designing and developing streaming media server in Lanxun, the largest CDN company in China at that time. Different from the REQUEST-Response model of HTTP, the protocols of streaming media, such as RTMP, are very complex and have many intermediate states. Especially, when cluster Edge is achieved, the interaction with upstream server will lead to the doubling of the state machine of the system. I consulted Michael, the architect at the company’s north American r&d center, and Michael recommended that I use a technology called ST(StateThreads) to solve this problem. ST actually implements user-mode threads, or coroutines, using setjmp and longjmp. Coroutines are similar to Goroutine, which are lightweight threads in user space. I didn’t understand why I was using a coroutine that I didn’t understand at the time, but after I spent some time learning about ST, it suddenly became clear that there are several typical concurrency models for server concurrency. Super complex state machines in streaming media servers, It also widely exists in various server domains, belonging to a kind of Essential Complexity that cannot be removed in this complex protocol server domain.

High performance, high concurrency, high scalability and readability of the network server architecture: State Threads for Internet Applications State Threads for Internet Applications Goroutine is a language level implementation of Go, and they essentially solve the same domain problems. Of course, Goroutine is more extensive, ST is just a network library. Let’s take a look at the nature of the goals of concurrency and start with the performance and scalability issues associated with concurrency:

  • On the horizontal axis is the number of clients, and on the vertical axis is the throughput rate, which is the amount of data that can be spit out to properly provide the service. For example, if 1,000 clients watch video at 500Kbps bit rate, that means each client needs 500Kb of data per second. Then the server needs to spit out 500*1000Kb=500Mb data per second to provide normal services, if the server due to performance problems CPU run full can not reach 500Mbps throughput rate, the client will start to stall;

  • The black line is the minimum throughput required by clients. Assuming that all clients are the same, the black line is a straight line with a fixed slope, i.e. the more clients there are, the more throughput there is, which is basically proportional to the number of clients. For example, one client needs 500Kbps throughput, and 1000 clients need 500Mbps throughput;

  • The solid blue line in the figure is the actual throughput that the server can achieve. When there are fewer clients, the server (if necessary) can handle data at a minimum throughput rate that exceeds the minimum required by the client due to the idle CPU. For example, in the case of a vod server, the client needs at least 500Kb of data per second to watch video on demand at a bit rate of 500Kbps. So the server can at 800Kbps throughput rate to the client data, so that the client will not naturally lag, the client will save the data in their own buffer, but if the user gives up playing this video will lead to the cache of data waste;

  • The solid blue line in the figure has a ceiling, which is the maximum throughput of the server on a given CPU resource. For example, a version of the server can only achieve 1Gbps throughput on 4 cpus due to performance issues. The intersection of the black line and the blue line is the maximum number of clients that the server can normally serve, say 2000. Theoretically, if the number of clients exceeds this maximum value, such as 10K, the server throughput will remain at the maximum value, such as 1Gbps. However, as the number of clients continues to increase, the system resources need to be consumed. For example, 10K FD and thread switching will preempt THE CPU time used for network sending and receiving, so the blue dotted line will appear. In other words, the throughput rate of the overloaded server decreases and the server cannot properly serve connected clients.

  • Load Scalability is the intersection between the black line and the blue line. The Scalability of the system depends on how well it can fit into the system, or whether the concurrent model can use CPU resources on network throughput rather than program switching. For example, a multi-process server may have very poor Load scaling. Some idle clients also Fork a process service, which is a waste of CPU resources. Simultaneous multi-process systems scale well, and throughput is almost linear as CPU resources are increased;

  • System Scalability refers to whether the throughput increases linearly with System resources, for example, doubling the number of cpus. The green line is doubling the CPU, so good system scalability should also double the throughput of the system. For example, in multi-threaded programs, due to the locking of competing resources or multi-thread synchronization, the increased CPU can not be fully used for throughput, and the system scalability of multi-threaded model is not as good as multi-process model.

There are several concurrent models, summarizing Existing Architectures in the table below:

Arch Load Scalability System Scalability Robust Complexity Example
Multi-Process Poor Good Great Simple Apache1.x
Multi-Threaded Good Poor Poor Complex Tomcat, FMS/AMS
Event-Driven

State Machine
Great Great Good Very

Complex
Nginx, CRTMPD
StateThreads Great Great Good Simple SRS, Go
  • Multi-process (MP) model: Each connection forks a Process service. The robustness of the system is very good. Connections are isolated from each other, and even if a process dies, other connections are not affected. Load Scalability is Poor because switching between a large number of processes is too expensive to use as much CPU time as possible on a network; for example, a 4-CPU server with 1000 busy processes cannot perform normal service. The System Scalability is very good, and the System throughput increases linearly when the CPU is increased. It is rare to see a pure multi-process server, especially one connected to one process. Although the performance is very low, the system complexity is low (Simple), the process is very independent, do not need to deal with locks or state;

  • MT(multi-threaded) model: some are one thread per connection, the improved model is split according to the responsibilities, such as read-write split threads, several threads read, several threads write. The system is not robust. A problem in one connection or thread affects other threads, affecting each other. The Scalability of threads is Good. Threads are lighter than processes; multiple user threads correspond to a kernel thread; however, the performance of the thread may be reduced to the same extent as that of multiple processes when the thread is blocked. The System Scalability is Poor because threads are synchronized, and even if locks are avoided in user space, they also occur at the kernel layer. When increasing cpus, there is generally loss in multithreading, and you don’t get the almost linear throughput increase that multiple processes get. The complexity of multithreading is also relatively high, mainly due to concurrency and lock introduction.

  • Event-driven State Machine (EDSM) An event-driven State Machine. For example, select/poll/epoll is usually a single process and a single thread, which can avoid multi-process locking problems. In order to avoid one-way system scaling problems, multi-process single thread can be used, such as NGINX is this way. The system is robust (Good), a process serves a part of the client, there is some isolation. Load Scalability is Great, with no process or thread switching, minimal overhead in user space, and almost all the CPU can be used on network throughput. The System Scalability is very good, and the throughput increases linearly during multi-process scaling. Although Very efficient, it is also Very Complex, requiring maintenance of Complex state machines, especially two coupled state machines, such as the state machine of the client service and the state machine of the back source.

  • ST(StateThreads) coroutine model. On the basis of EDSM, the problem of complex state machine is solved by opening up the stack of coroutines from the heap, saving the state in the stack, and actively switching (setjMP/longjMP) to other coroutines to complete IO when asynchronous IO wait (EAGAIN). In other words, ST integrates the advantages of EDSM and MT. However, ST threads are user-space threads rather than system threads. User-space threads also have scheduling overhead, which is much smaller than that of the system. The scheduling overhead of the coroutine is similar to that of the EDSM’s large loop, which loops each active client for processing one by one. The main problem of ST lies in platform adaptation. Since the setjMP/LongjMP of Glibc is encrypted and cannot modify the SP stack pointer, ST implements this logic by itself, and needs to adapt itself to different platforms. Currently, Linux supports it better, but Windows does not. In addition, this library is not maintained and some pits can only be circumnavigated, which is relatively remote and has few maintainers. For example, ST Patch has repaired some problems.

I also put Go in the ST model, although it is multi-threaded + coroutine, unlike SRS which is multi-process + coroutine (SRS itself is single-process + coroutine and can be extended to multi-process + coroutine).

From the concurrency model to see the Goroutine of Go, Go has the advantages of ST, there is no disadvantage of ST, this is the Go concurrency model powerful place. Of course, multi-threading of Go has some overhead, which is not as high as the load scalability of pure multi-process single thread. When there are too many active connections, multiple physical threads may be activated, resulting in performance degradation. That is, the performance of Go will be worse than that of ST or EDSM, and these performance are exchanged for system maintenance, which I personally think is worth it. In addition to Goroutine, the other key is Chan. The concurrency of Go is actually not just goroutine, but goroutine+chan, which is used to synchronize between multiple Goroutines. In fact, on top of these two mechanisms, and the context in the standard library, these three axes are Go’s concurrency mace.

  • Goroutine: Go language level native support for coroutines, a Go can start a coroutine, ST is implemented by functions;

  • Chan and SELECT: The communication mechanism between goroutines. ST can only implement queue and cond if it wants to implement message passing and waiting of two coroutines. What if you want to synchronize more than one? For example, if a coroutine handles multiple messages, including user cancellations, timeouts, and events from other threads, Go provides the select keyword. Refer to Share Memory By Communicating;

  • Context: The component that manages goroutine. See How GOLANG uses context to manage goroutine associations and how GOLANG uses context to pass values, timeout, and cancel. See Go Concurrency Patterns: Timing Out, Moving On, and Go Concurrency Patterns: Context.

Because Go is multithreaded, and the same thing about multithreading or coroutines, except chan also provides Mutex, both of which are actually usable, and sometimes it’s better to use chan than Mutex, sometimes it’s better to use Mutex than chan, See Mutex or Channel.

Channel Mutex
passing ownership of data,

distributing units of work,

communicating async results
caches,

state

Special reminder: don’t be afraid to use Mutex, don’t use chan for everything, a thousand-li horse can catch a thousand miles in a day but can’t catch mice, HelloKitty can’t run much faster to catch mice but is better than a thousand-li horse.

Context

In fact, goroutine management is very necessary in really high availability programs, and we generally need to support several Goroutine otine controls:

  1. Error handling: For example, when the underlying function fails, do we ignore and alarm (for example, only one connection is affected) or interrupt the whole service (for example, the LICENSE expires)?

  2. User cancellation: For example, when upgrading, we need to actively migrate new requests to new services, or cancel some long-running Goroutine, which is called hot upgrade;

  3. Timeout closure: For example, if the maximum request duration is 30 seconds, we should cancel the request after this time. Typically, the client’s service response is time-limited;

  4. Disassociation: for example, when a client requests a server, the server has to request many back-end services. If the intermediate client closes the connection, the server should abort, rather than continue to request all back-end services.

For Goroutine management, starting with only Chan and Sync, we need to manually implement goroutine lifecycle management. See Go Concurrency Patterns: Timing out, moving On, and Go Concurrency Patterns: Context are all part of goroutine’s Concurrency paradigm.

It was too cumbersome to manage Goroutine directly with the original components, and later context libraries appeared in large projects and became part of the standard library after Go1.7. See GOLANG’s use of Context to manage goroutine associations and GOLANG’s use of Context to implement value passing, timeout, and cancellation.

Context also has problems:

  1. Cancel, Timeout, and Value are supported, which are nodes that expand the Context tree. Cancel and Timeout will delete the subtree when the subtree is canceled, and will not remain inflated. Values do not provide deletion functions, and if they have common root nodes, this will cause the Context tree to grow larger and larger; So the Context of type Value should hang below the Context tree of Cancel so that the GC will reclaim it upon cancellation;

  2. Can cause inconsistent or strange interfaces, such as IO.Reader, when the first argument should be context, such as Read(context, []byte). Or provide two sets of interfaces, one with Contex and one without Context. This can be a bit confusing, and generally in applications, the recommended first argument is Context;

  3. Note the Context tree, if the tree gets deeper and deeper due to Closure, there will be performance issues with the call stack. For example, 100,000 long chains would result in CPU usage of about 500%.

Note: Context should go away for go 2 is criticized for adding Context as the first argument in the standard library, such as Read(CTX context.context, etc.).

Go development technical Guide series of articles

  • Why did you choose Go? (Contains a large picture of super-complete knowledge)
  • Go Failure-oriented programming

Cloud Native technology open class

This course is a series of technical open courses jointly launched by CNCF and Alibaba, which take “cloud native technology system” as the core and pay equal attention to “technical interpretation” and “practice landing”.

“Alibaba Cloud originators pay close attention to technical fields such as microservice, Serverless, container and Service Mesh, focus on cloud native popular technology trends and large-scale implementation of cloud native, and become the technical circle that knows most about cloud native developers.”