
Go language: rehabilitation of imperative programming
Almost all modern programming languages include object-oriented features in one form or another, however, Go authors have tried to limit themselves to the imperative paradigm as much as possible. This should not come as a surprise, given that one of the authors of the language is Ken Thompson (developer of UNIX and C). Such a pronounced imperative of the language can lead the experienced object-oriented programmer to some bewilderment and sow doubts about the possibility of solving modern problems in such a language.
This article is intended to help programmers interested in Go to understand the imperative features of the language. In particular, help implement key design patterns. In addition, some interesting solutions implemented in Go itself, its standard library and tools, which will pleasantly surprise many, will be given.
As in many imperative programming languages (C / Algol / Pascal, etc.), the key entity is structure. Structures are defined in Go as follows:
In addition to structures, aliases can be declared in the same way:
To create a variable containing an instance of the structure, there are several ways to do this:
The names of the structure fields during initialization can be omitted while maintaining the declaration sequence:
Because Since Go has a built-in garbage collector, there is no difference between variables instantiated directly or through a link.
Exiting a link from the scope does not lead to a memory leak, and a variable instantiated by value is not freed if at least one link exists, including out of scope.
Those. The following code is completely safe, although similar constructions in C / C ++ can lead to fatal consequences:
There is no habitual inheritance in Go, however, if we consider inheritance as a transmission mechanism a) belonging to a certain type, b) transmission of a certain behavior and c) transmission of basic fields , then anonymous fields and interfaces can be attributed to such inheritance mechanisms.
Anonymous fields avoid duplicate field descriptions in structures. So, for example, if there is some User structure, and on the basis of this structure it is necessary to make a few more specific ones: Buyer Buyer and Cashier cashier, then the fields for new structures can be borrowed from User as follows:
Despite the fact that User is not connected by “family ties” and nothing will say that Buyer is the descendant of User, the fields of the User structure will be available in Buyer / Cashier.
On the other hand, it is now necessary to implement the methods for User / Buyer / Cashier separately, which is not very convenient, because 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 is the method of sending a message to SendMail (text string). Because the only thing that is required from each of the structures is Email, then it is enough to make an interface with the requirement of the GetEmail method.
Go has no access modifiers. The availability of a variable, structure, or function depends on the identifier.
Go exports only entities whose identifier satisfies both conditions:
In other words, to hide the identifier, just name it with a small letter.
In essence, Go lacks ad-hoc polymorphism, there is no parametric polymorphism (i.e. Java generics and c ++ templates) and there is no explicit polymorphism of subtypes.
In other words, it is impossible to define two functions with the same name and different signature in the same module, and it is also impossible to make a common method for different types.
Those. all of the following constructs in Go are illegal and lead to compilation errors:
However, Go has two mechanisms that emulate polymorphic behavior.
This is, firstly, dynamic type dispatching, and secondly, duck typing.
So any object in Go can be reduced to the interface {} type, which allows passing variables of any type to the function:
Because interface {} cannot have its own methods, in order to return access to the type there is a special switch type construction:
Go has no constructors or destructors. In order to create an instance of a complex structure, special functions starting with New are defined, for example:
The presence of such a constructor function does not limit the ability to instantiate the structure directly. However, this approach is used even in the standard Go library and helps to organize code in large applications.
The situations with destructors in Go are much more complicated. similar functionality similar to that available in C ++ cannot be fully implemented.
If you need to free resources, you can make the Release method:
Of course, this method will not be called on its own if the variable goes out of scope or in case of an exception, as happens in C ++ (there are no exceptions in Go). In such situations, it is proposed to use the defer, panic, and recover mechanism . For example, the Release method can be delayed using the defer directive:
This allows you to free resources after calling the Foo function, regardless of the scenario.
Defer behavior is always predictable and is described by three rules:
The built-in panic and recover functions act as a replacement for exceptions :
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 a panic / recover function, you can achieve the same security that is achieved in object-oriented languages using try / catch constructs. In particular, to prevent the leak of resources and the unexpected termination of the program.
If the moment of destruction of an instance of the 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 the release of the instance by the garbage collector.
So, the described mechanisms allow us to solve the same problems as inheritance, encapsulation, polymorphism solve in object-oriented programming. The presence of duck typing, coupled with interfaces, presents almost the same possibilities as conventional inheritance in object-oriented languages. This is well illustrated by the implementation of some key classic design patterns below.
Go does not have a static modifier, when a static variable is required, it is carried out into the package body. The Singleton pattern is built on this solution in the simplest case:
All three patterns are based on the implementation of some abstract interface, which allows you to control the creation of specific products through the implementation of their own methods-creators. An interface declaration might look like this:
The implementation of one-to-one methods of specific structures corresponds to the implementation in object-oriented programming.
Examples can be viewed on github:
Abstract factory ;
Factory method ;
The builder .
Very often, the Prototype pattern is replaced simply with surface copy structure:
In the general case, the problem is solved in the classical way, by creating an interface with the Clone method:
An example implementation can be viewed on github: Prototype .
The use of the RAII pattern is complicated by the lack of a destructor, so to get a more or less acceptable behavior, you need to use the runtime.setFinalizer function, which passes a pointer to a method that frees previously occupied resources.
Implementation Example:
RAII .
All four patterns are very similar, are constructed in the same way, so it’s enough to give only the adapter implementation:
The linker is even easier to implement, as only two interfaces Composite (describing the structural behavior) and Component (describing user-defined functions) are enough:
An example implementation of the pattern: Linker .
A very common pattern in Go, though it is implemented mainly through anonymous function handlers. They can be found in large numbers, for example, in the package net / http standard library. In the classic version, the pattern looks like this:
Implementation example: Responsibility chain .
As shown, almost all classical design patterns can be reproduced in a language. However, this is not the main advantage of the language. Support for goroutine-based multithreading, data channels between threads, support for anonymous functions and context closure, easy integration with C-libraries, as well as a powerful standard package library are very important. All this is worth a separate careful consideration, which of course is beyond the scope of the article.
No less surprising are other innovations in the language, which are more related to the infrastructure of the language than to the language itself. However, every experienced programmer will appreciate them.
In Go, everything is divided into packages, just like in Java, everything is divided into classes. The main package that starts the program execution should be called main. Each package is usually a more or less independent part of the program, which is included in main through import. For example, to use the standard mathematical package, just enter import “math” . The path to the package can also be the address of the repository. A simple OpenGL program might look like this:
In order to download all the dependencies, just go get from the project directory.
It is always possible to read documentation from the command line using the godoc command. For example, to get the description of the Sin function from the math package, just enter the godoc math sin command:
Also on the local machine, you can start the golang.com server clone if the Internet was unavailable for some reason:
Learn more about godoc .
Sometimes in the code you need to make uniform changes, for example, to rename using a pattern or to correct homogeneous mathematical expressions. The gofmt tool is provided for this:
Replaces all expressions of the form bytes.Compare (a, b) with bytes.Equal (a, b). Even if the variables will be called differently.
Gofmt can also be used to simplify common expressions with the -s flag. This flag is similar to the following substitutions:
Also gofmt can be used to save code style in the project. More on gofmt
Go includes a special testing testing package . To create tests for the package, just make the file of the same name with the suffix "_testing.go". All tests and benchmarks start with Test or Bench:
To run the tests, the go test utility is used. With it, you can run tests, measure coverage, run benchmarks, or run a test on a pattern. Using the gopatterns project created to describe and test the patterns of this article as an example, it looks like this:
So, despite the fact that Go is built on an imperative paradigm, however, it has enough funds to implement classical design patterns. In this regard, it is in no way 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 language infrastructure, built-in refactoring and documenting tools significantly distinguish the language from competitors, as things like this are usually implemented by the community.
All this, even without a detailed examination of goroutine, channels, the interface with native libraries.
In general, Go has shown that imperative and structural 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.
This article is intended to help programmers interested in Go to understand the imperative features of the language. In particular, help implement key design patterns. In addition, some interesting solutions implemented in Go itself, its standard library and tools, which will pleasantly surprise many, will be given.
Introduction: Types, Structures, and Variables
As in many imperative programming languages (C / Algol / Pascal, etc.), the key entity is structure. Structures are defined in Go as follows:
type User struct{
Name string
Email string
Age int
}
In addition to structures, aliases can be declared in the same way:
type UserAlias User
type Number int
type UserName string
To create a variable containing an instance of the structure, there are several ways to do this:
// Объявить переменную по значению
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}
Because Since Go has a built-in garbage collector, there is no difference between variables instantiated directly or through a link.
Exiting a link from the scope does not lead to a memory leak, and a variable instantiated by value is not freed if at least one link exists, including out of scope.
Those. The following code is completely safe, although 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 transmission mechanism a) belonging to a certain type, b) transmission of a certain behavior and c) transmission of basic fields , then anonymous fields and interfaces can be attributed to such inheritance mechanisms.
Anonymous fields avoid duplicate field descriptions in structures. So, for example, if there is some User structure, and on the basis of this structure it is necessary to make a few more specific ones: 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 connected by “family ties” and nothing will say that Buyer is the descendant of User, the fields of the User structure will be available in Buyer / Cashier.
On the other hand, it is now necessary to implement the methods for User / Buyer / Cashier separately, which is not very convenient, because 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 is the method of sending a message to SendMail (text string). Because the only thing that is required from each of the structures is Email, then it is enough to make an interface with the requirement of 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
Go has no access modifiers. The availability of a variable, structure, or function depends on the identifier.
Go exports only entities whose identifier satisfies both conditions:
- The identifier begins with a capital letter (Unicode class "Lu")
- The identifier is declared in the package block (i.e. is not nested anywhere), or is the name of a method or field
In other words, to hide the identifier, just name it with a small letter.
Type dispatch
In essence, Go lacks ad-hoc polymorphism, there is no parametric polymorphism (i.e. Java generics and c ++ templates) and there is no explicit polymorphism of subtypes.
In other words, it is impossible to define two functions with the same name and different signature in the same module, and it is also 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 emulate polymorphic behavior.
This is, firstly, dynamic type dispatching, and secondly, duck typing.
So any object in Go can be reduced to the interface {} type, which allows passing variables of any type to the function:
package main
func Foo(v interface{}) {
}
func main() {
Foo(123)
Foo("abs")
}
Because interface {} cannot have its own methods, in order to return access to the type there is a special switch type construction:
func Foo(v interface{}) {
switch t := v.(type) {
case int:
// здесь переменная t имеет тип int
case string:
// здесь переменная t имеет тип string
default:
// неизвестный тип
}
}
Variable lifetime management
Go has no constructors or destructors. In order to create an instance of a complex structure, special functions starting with New are defined, 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 instantiate the structure directly. However, this approach is used even in the standard Go library and helps to organize code in large applications.
The situations with destructors in Go are much more complicated. similar functionality similar to that available in C ++ cannot be fully implemented.
If you need to free resources, you can make the Release method:
func (r *Resource) Release() {
// release resources
}
Of course, this method will not be called on its own if the variable goes out of scope or in case of an exception, as happens in C ++ (there are no exceptions in Go). In such situations, it is proposed to use the defer, panic, and recover mechanism . 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 resources after calling the Foo function, regardless of the scenario.
Defer behavior is always predictable and is described by three rules:
- Arguments of a deferred function are calculated at the time the defer construct is formed;
- Deferred functions are called in the order “last entered - first left” after the message of the framing function is returned;
- Deferred functions can read and modify named return values.
The built-in panic and recover functions act as a replacement for exceptions :
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 a panic / recover function, you can achieve the same security that is achieved in object-oriented languages using try / catch constructs. In particular, to prevent the leak of resources and the unexpected termination of the program.
If the moment of destruction of an instance of the 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 the release of the instance by the garbage collector.
Design patterns
So, the described mechanisms allow us to solve the same problems as inheritance, encapsulation, polymorphism solve in object-oriented programming. The presence of duck typing, coupled with interfaces, presents almost the same possibilities as conventional inheritance in object-oriented languages. This is well illustrated by the implementation of some key classic design patterns below.
Singleton - Singleton
Go does not have a static modifier, when a static variable is required, it is carried out into the package body. The Singleton pattern is built on this solution 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, which allows you to control the creation of specific products through the implementation of their own methods-creators. An 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 specific 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 replaced simply with surface copy 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, by creating 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 to get a more or less acceptable behavior, you need to use the runtime.setFinalizer function, which passes a pointer to a method that frees previously occupied resources.
type Resource struct{
}
func NewResource() *Resource {
// здесь происходит захват ресурса
runtime.SetFinalizer(r, Deinitialize)
return r
}
func Deinitialize(r *Resource) {
// метод освобождающий ресурсы
}
Implementation Example:
RAII .
Adapter. Decorator. Bridge. Facade - Adapter. Bridge Decorator Facade
All four patterns are very similar, are constructed in the same way, so it’s enough to give only the adapter implementation:
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, as only two interfaces Composite (describing the structural behavior) and Component (describing user-defined functions) are enough:
type Component interface {
GetName() string
}
type Composite interface {
Add(c Component)
Remove(c Component)
GetChildren() []Component
}
An example implementation of the pattern: Linker .
Chain of responsibility - Chain of responsibility
A very common pattern in Go, though it is implemented mainly through anonymous function handlers. They can be found in large numbers, for example, in the package net / http 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)
}
}
Implementation example: Responsibility chain .
Nice Go Features
As shown, almost all classical design patterns can be reproduced in a language. However, this is not the main advantage of the language. Support for goroutine-based multithreading, data channels between threads, support for anonymous functions and context closure, easy integration with C-libraries, as well as a powerful standard package library are very important. All this is worth a separate careful consideration, which of course is beyond the scope of the article.
No less surprising are other innovations in the language, which are more related to the infrastructure of the language than to the language itself. However, every experienced programmer will appreciate them.
Built-in package manager with support for git, hg, svn and bazaar
In Go, everything is divided into packages, just like in Java, everything is divided into classes. The main package that starts the program execution should be called main. Each package is usually a more or less independent part of the program, which is included in main through import. For example, to use the standard mathematical package, just enter import “math” . The path to the package can also be the address of the repository. 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, just go get from the project directory.
Local Go documentation
It is always possible to read documentation from the command line using the godoc command. For example, to get the description of the Sin function from the math package, just enter the godoc math sin command:
$ 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
Also on the local machine, you can start the golang.com server clone if the Internet was unavailable for some reason:
$ godoc -http=:6060
Learn more about godoc .
Command line refactoring and formatting
Sometimes in the code you need to make uniform changes, for example, to rename using a pattern or to correct homogeneous mathematical expressions. The gofmt tool is provided for this:
gofmt -r 'bytes.Compare(a, b) == 0 -> bytes.Equal(a, b)'
Replaces all expressions of the form bytes.Compare (a, b) with bytes.Equal (a, b). Even if the variables will be called differently.
Gofmt can also be used 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 {...}
Also gofmt can be used to save code style in the project. More on gofmt
Unit Testing and Benchmarks
Go includes a special testing testing package . To create tests for the package, just 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 < b.N; i++ {
fmt.Sprintf("hello")
}
}
To run the tests, the go test utility is used. With it, you can run tests, measure coverage, run benchmarks, or run a test on a pattern. Using the gopatterns project created to describe and test the patterns of this article as an example, 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 is built on an imperative paradigm, however, it has enough funds to implement classical design patterns. In this regard, it is in no way 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 language infrastructure, built-in refactoring and documenting tools significantly distinguish the language from competitors, as things like this are usually implemented by the community.
All this, even without a detailed examination of goroutine, channels, the interface with native libraries.
In general, Go has shown that imperative and structural 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.