📜 ⬆️ ⬇️

Go language: rehabilitation of imperative programming

Virtually all modern programming languages ​​include object-oriented features in one form or another, however, the authors of the Go language tried to limit themselves to the imperative paradigm as much as possible. This should not come as a surprise, considering that one of the authors of the language is Ken Thompson (the developer of UNIX and C). Such a strongly pronounced imperative language can introduce an experienced object-oriented programmer to some confusion and sow doubts about the possibility of solving modern problems in such a language.

This article is designed to help programmers who are interested in Go, to understand the imperative features of the language. In particular, help implement key design patterns. In addition, there will be some interesting solutions implemented in Go itself, its standard library and toolkit, which will pleasantly surprise many.

Introduction: Types, Structures, and Variables


As in many imperative programming languages ​​(C / Algol / Pascal, etc.), structure is a key entity. Structures are defined in Go as follows:

type User struct{ Name string Email string Age int } 

In addition to structures, similarly, you can declare aliases:
')
 type UserAlias User type Number int type UserName string 

To create a variable containing an instance of a structure, there are several ways to proceed:

 //     var user0 User //       user1 := User{} //    user2 := make(User, 1) user3 := &User{} //         nil var user4 *User 

The names of the structure fields during initialization can be omitted while maintaining the declaration sequence:

 u1 := User{Name: "Jhon", Email: "jhon@example.or", Age: 27} u2 := User{"Jhon", "jhon@example.or", 27} 

Since Go has a built-in garbage collector, then there is no difference between the variables instantiated directly or via a link.
The exit of the link from the zone of visibility does not lead to a memory leak, and the variable instantiated by value is not released if at least one link exists, including out of scope.
Those. The following code is absolutely safe, even though similar constructions in C / C ++ can lead to fatal consequences:

 type Planet struct{ Name string } func GetThirdPlanetByRef() *Planet{ var planet Planet planet.Name = "Earth" return &planet } func GetThirdPlanetByVal() Planet{ var planet *Planet planet = &Planet{Name: "Earth"} return *planet } 


Interfaces and anonymous fields instead of inheritance


There is no habitual inheritance in Go, however, if we consider inheritance as a transfer mechanism for a) belonging to a certain type, b) passing certain behavior and c) transferring basic fields , then anonymous fields and interfaces can be attributed to such inheritance mechanisms.

Anonymous fields allow you to avoid duplication of field descriptions in structures. So, for example, if there is a certain User structure, and based on this structure, you need to make some more specific: Buyer Buyer and Cashier cashier, then the fields for new structures can be borrowed from User as follows:

 type Buyer struct { User Balance float64 Address string } type Cashier struct { User InsurenceNumber string } 

Despite the fact that User is not related by “family ties” and nothing will say that Buyer is an heir from User, the fields of the User structure will also be available in Buyer / Cashier.

On the other hand, it is now necessary to implement methods for User / Buyer / Cashier separately, which is not very convenient, since leads to gigantic duplication.
Instead, methods that implement the same behavior can be converted to functions that take a common interface as an argument. An example would be the method of sending a message to the SendMail mail (text string). Since the only thing that is required from each of the structures is Email, it is enough to make an interface with the requirement for the GetEmail method.

 type UserWithEmail interface { GetEmail() string } func SendMail(u *UserWithEmail, text string) { email := u.GetEmail() //    email } func main() { //  users      users := []UserWithMail{User{}, Buyer{}, Cashier{}} for _, u := range users { SendEmail(u, "Hello world!!!") } } 


Encapsulation


There are no access modifiers in Go. The availability of a variable, structure, or function depends on the identifier.
Go exports only those entities whose identifier satisfies both conditions:

  1. The identifier begins with a capital letter (Unicode class "Lu")
  2. The identifier is declared in the package block (that is, it is not nested anywhere), or is the name of a method or field

In other words, to hide an identifier it is enough to name it with a small letter.

Type Dispatch


In fact, Go has no ad-hoc polymorphism, no parametric polymorphism (i.e., Java generics and c ++ templates) and no explicit polymorphism of subtypes.
In other words, it is impossible to define two functions with the same name and different signatures in the same module, as well as it is impossible to make a common method for different types.
Those. all of the following constructs in Go are illegal and lead to compilation errors:

 func Foo(value int64) { } //   "Foo redeclared in this block", ..    func Foo(value float64) { } type Base interface{ Method() } //   "invalid receiver type Base (Base is an interface type)", ..      func (b *Base) Method() { } 

However, Go has two mechanisms that allow you to emulate polymorphic behavior.
This is, firstly, dynamic type dispatching, and secondly, duck typing.

So any object in Go can be reduced to type interface {}, which allows passing variables of arbitrary type to the function:

 package main func Foo(v interface{}) { } func main() { Foo(123) Foo("abs") } 

Since interface {} cannot have its own methods, then in order to return access to the type there is a special construction of the switch type:

 func Foo(v interface{}) { switch t := v.(type) { case int: //   t   int case string: //   t   string default: //   } } 


Variable Lifetime Management


In Go, there are no constructors and destructors. In order to create an instance of a complex structure, special functions begin with New, for example:

 func NewUser(name, email string, age int) *User { return &User{name, email, age} } 

The presence of such a constructor function does not limit the ability to create an instance of the structure directly. However, this approach is used even in the standard Go library and helps to systematize code in large applications.

Situations with destructors in Go are much more difficult, since similar functionality similar to that found in C ++ cannot be fully implemented.

If you need to release resources, you can make the Release method:

 func (r *Resource) Release() { // release resources } 


Of course, this method will not be called by itself if the variable goes out of scope or in the case of an exception, as it does in C ++ (besides, there are no exceptions in Go). In such situations, it is proposed to use the mechanism of defer, panic and recover . For example, the Release method can be delayed using the defer directive:

 func Foo() { r := NewResource() defer r.Release() if err := r.DoSomething1(); err != nil { return } if err := r.DoSomething2(); err != nil { return } if err := r.DoSomething3(); err != nil { return } } 

This allows you to free up resources after calling the Foo function, regardless of the scenario.
The behavior of defer is always predictable and is described by three rules:

  1. Arguments of the deferred function are calculated at the moment when the defer structure is formed;
  2. Deferred functions are called in the order “last entered - first out” after returning the message of the framing function;
  3. Deferred functions can read and modify named return values.

As replacement of exceptions, the built-in panic and recover functions are:

 func Bar() { panic("something is wrong") } func Foo() { defer func() { if r := recover(); r != nil { fmt.Println("Recovered in Bar: ", r) } }() Bar() fmt.Prinln("this message will not be printed on panic inside Bar") } 

Panic causes all framing functions to terminate, so the only way to stop the spread of panic is to call the recover () function. By combining the use of defer-expressions and panic / recover functions, you can achieve the same security that is achieved in object-oriented languages ​​using try / catch constructs. In particular, to prevent the leakage of resources and the unexpected end of the program.

If the moment of the destruction of an instance of a structure is unpredictable, then the only way in Go to release resources is to use the SetFinalizer function from the standard runtime package. It allows you to catch the moment of release of the instance by the garbage collector.

Design Patterns


So, the described mechanisms allow solving the same problems as inheritance, encapsulation, and polymorphism are solved in object-oriented programming. The presence of duck typing, coupled with interfaces, represents almost the same possibilities as regular inheritance in object-oriented languages. This is well illustrated by the implementation of some key classic design patterns given below.

Singleton - Singleton


In Go, there is no static modifier, when a static variable is required, it is put into the package body. The Singleton pattern is built on this decision in the simplest case:

 type Singleton struct{ } //         var instance *Singleton func GetSingletonInstance() *Singleton { if instance == nil { instance = &Singleton{} } return instance } 


Abstract factory. Factory method. Builder - Abstract factory. Factory method. Builder


All three patterns are based on the implementation of some abstract interface that allows you to manage the creation of specific products through the implementation of their own methods, the creators. The interface declaration might look like this:

 type AbstractProduct interface{ } //   type AbstractFactory interface { CreateProduct1() AbstractProduct CreateProduct2() AbstractProduct } //   type AbstractCreator interface { FactoryMethod() AbstractProduct } //  type AbstractBuilder interface { GetResult() AbstractProduct BuildPart1() BuildPart2() } 

The implementation of one-to-one methods of concrete structures corresponds to the implementation in object-oriented programming.

Examples can be viewed on github:

Abstract Factory ;
Factory method ;
The builder .

Prototype - Prototype


Very often, the Prototype pattern is simply replaced with a superficial copy of the structure:

 type T struct{ Text string } func main(){ proto := &T{"Hello World!"} copied := &T{} //   *copied = *proto if copied != proto { fmt.Println(copied.Text) } } 


In the general case, the problem is solved in the classical way, through the creation of an interface with the Clone method:

 type Prototype interface{ Clone() Prototype } 


An example implementation can be viewed on github: Prototype .

RAII


The use of the RAII pattern is complicated by the lack of a destructor, so in order to get a more or less acceptable behavior, you need to use the runtime.setFinalizer function to which the pointer to the method that releases the previously used resources is passed.

 type Resource struct{ } func NewResource() *Resource { //     runtime.SetFinalizer(r, Deinitialize) return r } func Deinitialize(r *Resource) { //    } 


Example of implementation:

RAII .

Adapter. Decorator. Bridge. Facade - Adapter. Bridge. Decorator. Facade


All four patterns are very similar, are constructed in a similar way, so it suffices to give only the implementation of the adapter:

 type RequiredInterface interface { MethodA() } type Adaptee struct { } func (a *Adaptee) MethodB() { } type Adapter struct{ Impl Adaptee } func (a *Adapter) MethodA() { a.Impl.MethodB() } 


Linker - Composite


The linker is even easier to implement, since only two interfaces Composite (describing the structural behavior) and Component (describing the user functions) are sufficient:

 type Component interface { GetName() string } type Composite interface { Add(c Component) Remove(c Component) GetChildren() []Component } 

An example of the implementation of the pattern: Linker .

Chain of responsibility


The pattern is very common in Go, though it is implemented mainly through anonymous handler functions. They can be found in large numbers, for example, in the net / http package of the standard library. In the classic version, the pattern looks like this:

 type Handler interface{ Handle(msg Message) } type ConcreteHandler struct { nextHandler Handler } func (h *ConcreteHandler) Handle(msg Message) { if msg.type == "special_type" { // handle msg } else if next := h.nextHandler; next != nil { next.Handle(msg) } } 


Example implementation: chain of responsibility .

Nice features go


As it was shown, practically all classical design patterns can be reproduced in the language. However, this is not the main advantage of the language. Support for multithreading based on goroutine, data feeds between threads, support for anonymous functions and context closure, easy integration with C-libraries, as well as a powerful standard package library are also very important. All this is worth a separate careful consideration, which of course goes beyond the scope of the article.

No less surprising are other innovations in the language that relate more to the infrastructure of the language than to the language itself. However, they will be appreciated by every experienced programmer.

Built-in package manager with git, hg, svn and bazaar support


In Go, everything is divided into packages, just like in Java, everything is divided into classes. The main package, from which the program starts, should be called main. Each package is usually a more or less independent part of the program, which is included in the main via import. For example, to use the standard math package, just enter import “math” . The repository address can also act as a package path. A simple OpenGL program might look like this:

 package main import ( "fmt" glfw "github.com/go-gl/glfw3" ) func errorCallback(err glfw.ErrorCode, desc string) { fmt.Printf("%v: %v\n", err, desc) } func main() { glfw.SetErrorCallback(errorCallback) if !glfw.Init() { panic("Can't init glfw!") } defer glfw.Terminate() window, err := glfw.CreateWindow(640, 480, "Testing", nil, nil) if err != nil { panic(err) } window.MakeContextCurrent() for !window.ShouldClose() { //Do OpenGL stuff window.SwapBuffers() glfw.PollEvents() } } 


In order to download all the dependencies, it is enough to run go get from the project directory.

Local Go documentation


It is always possible to read the documentation from the command line using the godoc command. For example, to get a description of the Sin function from the math package, just enter the command godoc math sin:

 $ godoc math Sin func Sin(x float64) float64 Sin returns the sine of the radian argument x. Special cases are: Sin(±0) = ±0 Sin(±Inf) = NaN Sin(NaN) = NaN 


You can also run the golang.com server clone on the local machine, if the Internet for some reason was unavailable:

 $ godoc -http=:6060 


Read more about godoc .

Refactoring and formatting from the command line


Sometimes it is required to make uniform changes in the code, for example, renaming with a pattern or correct homogeneous mathematical expressions. For this, the gofmt tool is provided:

 gofmt -r 'bytes.Compare(a, b) == 0 -> bytes.Equal(a, b)' 


Replace all expressions of the form bytes.Compare (a, b) with bytes.Equal (a, b). Even if the variables will be called differently.

You can also use gofmt to simplify common expressions with the -s flag. This flag is similar to the following substitutions:

 []T{T{}, T{}} -> []T{{}, {}} s[a:len(s)] -> s[a:] for x, _ = range v {...} -> for x = range v {...} 


You can also use gofmt to save the code style in the project. Read more about gofmt

Unit testing and benchmarks


Go comes with a special package for testing testing . To create tests for the package, it is enough to make the file of the same name with the suffix "_testing.go". All tests and benchmarks start with Test or Bench:

 func TestTimeConsuming(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode.") } ... } func BenchmarkHello(b *testing.B) { for i := 0; i < bN; i++ { fmt.Sprintf("hello") } } 


To run the tests, use the go test utility. Using it, you can run tests, measure coverage, run benchmarks, or run a pattern test. Using the example of the gopatterns project created to describe and check the patterns of this article, it looks like this:

 $ go test -v === RUN TestAbstractFactory --- PASS: TestAbstractFactory (0.00 seconds) === RUN TestBuilder --- PASS: TestBuilder (0.00 seconds) === RUN TestChain --- PASS: TestChain (0.00 seconds) === RUN TestComposite --- PASS: TestComposite (0.00 seconds) === RUN TestFactoryMethod --- PASS: TestFactoryMethod (0.00 seconds) === RUN TestPrototype --- PASS: TestPrototype (0.00 seconds) === RUN TestRaii --- PASS: TestRaii (1.00 seconds) === RUN TestSingleton --- PASS: TestSingleton (0.00 seconds) PASS ok gopatterns 1.007s $ go test -cover PASS coverage: 92.3% of statements $go test -v -run "Raii" === RUN TestRaii --- PASS: TestRaii (1.00 seconds) PASS ok gopatterns 1.004s 


Conclusion


So, despite the fact that Go and is built on an imperative paradigm, nevertheless, it has enough funds to implement the classic design patterns. In this regard, it is not inferior to popular object-oriented languages. At the same time, such things as the built-in package manager, support for unit tests at the level of the infrastructure of the language, the built-in means of refactoring and documenting the code noticeably distinguish the language among competitors, since such things are usually implemented by the community.

All this, even without looking at the goroutine, channels, interface with native libraries.

In general, Go showed that imperative and structured programming does not go down in history. A modern language that meets the main trends in software development can be built on the basis of an imperative paradigm, no worse than on the basis of an object-oriented or functional paradigm.

Source: https://habr.com/ru/post/240917/


All Articles