Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases now! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Hands-On Software Architecture with Golang
Hands-On Software Architecture with Golang

Hands-On Software Architecture with Golang: Design and architect highly scalable and robust applications using Go

eBook
$29.99 $43.99
Paperback
$54.99
Subscription
Free Trial
Renews at $19.99p/m

What do you get with eBook?

Product feature icon Instant access to your Digital eBook purchase
Product feature icon Download this book in EPUB and PDF formats
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
Table of content icon View table of contents Preview book icon Preview Book

Hands-On Software Architecture with Golang

Building Big with Go

It's easy to solve small confined problems with limited constraints. It's also easy to comprehend and mentally model requirements and build a solution. However, as problems become more complex or constraints add up, problem-solving without a plan more often than not ends in failure. On the other hand, sometimes we overdo planning and are left with little room to react to new situations as they crop up. Architecture is the fine act of balancing the long versus the short.

This chapter asks the question: Why engineer software?. It outlines the elements needed for making and executing a blueprint for a successful software product. The topics covered in this chapter include the following:

  • Problem solving for the big picture and the role that the architect is supposed to play in this
  • The basic tenets of software architecture
  • A deep dive into microservices
  • Introduction to Golang

Problem solving for the big picture

Suppose you're planning a trip from New York to Los Angeles. There are two major aspects that you need to keep in mind:

  • What do you need to do before starting the trip?
  • What do you need to do during the trip to ensure that you stay on the right track?

Generally, there are two extreme options in planning for such a trip:

  • Take your car and start driving. Figure things out along the way.
  • Make a very detailed plan—figure out the route, note down the directions at every junction, plan for contingencies such as a flat tire, plan where you're going to stop, and so on.

The first scenario allows you to execute fast. The problem is that, more likely than not, your trip will be very adventurous. Most likely, your route will not be optimal. If you want to update your friend in LA on when you'll be reaching the destination, your estimates will vary wildly based on your current situation. Without a long-term plan, planning for an outcome in the future is best effort.

But the other extreme is also fraught with pitfalls. Every objective has time constraints, and spending time over-analyzing something might mean that you miss the bus (or car). More frequently, if you give these directions to someone else and, at a juncture, reality turns out not to be what you predicted, then the driver is left with little room to improvise.

Extending the analogy, the architecture of a software product is the plan for the journey of building a product that meets the requirements of the customers and other stakeholders, including the developers themselves!

Writing code for computers to understand and use to solve a problem has become easy. Modern tools do all of the heavy lifting for us. They suggest syntax, they optimize instructions, and they even correct some of our mistakes. But writing code that can be understood by other developers, works within multiple constraints, and evolves with changing requirements is an extremely hard thing to do.

Architecture is the shape given to a system by those who build it. Shape essentially means the constituent components, the arrangement of those components, and the ways in which those components communicate with each other. The purpose of that shape is to facilitate the development, deployment, operation, and maintenance of the software system contained within it. In today's world of ever changing requirements, building a platform on which we can execute quickly and effectively is the key to success.

The role of the architect

An architect is not a title or a rank; it's a role. The primary responsibility of the architect is to define a blueprint for what needs to be built and ensure that the rest of the team has sufficient details to get the job done. The architect guides the rest of the team toward this design during execution, while managing constant dialogues with all of the stakeholders.

The architect is also a sounding board for both developers and non-technical stakeholders, in terms of what is possible, what is not, and the cost implications (in terms of effort, trade-offs, technical debt, and so on) of various options.

It's possible to do the architect's job without coding. But in my personal opinion, this leads to stunted design. It's not possible to come up with a great design unless we understand the low-level details, constraints, and complexity. Many organizations dismiss the architect's role because of their negative experiences of architects that dictate from ivory towers and aren't engaged with the actual task of building working software. But, on the other hand, not having a blueprint can lead to a wild west code base, with small changes causing non-intuitive effects, in terms of effort and the quality of the product.

This book is not a theoretical study in software engineering. This book is meant for architects who want to build awesome, reliable, and high-performance products, while being in the kitchen as the product is getting built!

So what are the guidance systems or guard rails that the architect is expected to deliver on? Essentially, the team needs the following things from an architect.

Requirements clarification

Clarifying and distilling the top-level functional and nonfunctional requirements of the software is a key prerequisite for success. If you don't know what to build, your chances of building something that customers want are pretty slim. Product managers often get caught up on features, but rarely ask what non-functional requirements (or system qualities) the customers need. Sometimes, stakeholders will tell us that the system must be fast, but that's far too subjective. Non-functional requirements need to be specific, measurable, achievable, and testable if we are going to satisfy them. The architect needs to work with all stakeholders and ensure that functional and nonfunctional requirements are well crystallized and consumable for development.

In today's agile world, requirement analysis is an almost ongoing activity. But the architect helps the team navigate the requirements and take decisions on what to do (which may not always be so obvious).

True North

Besides the requirements, we need to define key engineering principles for the system. These principles include the following:

  • High-level design: This is the decomposition of the system into high-level components. This serves as the blueprint that the product and code need to follow at every stage of the product development life cycle. For example, once we have a layered architecture (see the following section), then we can easily identify for any new requirement to which layer each new component should go to.
  • Quality attributes: We want high quality code, and this means no code checking would be allowed without unit tests and 90% code coverage.
  • Product velocity: The product has a bounded value in time and, to ensure that there is high developer productivity, the team should build Continuous Integration / Continuous Deployment (CICD) pipelines from the start.
  • A/B testing: Every feature should have a flag, so that it can be shown only to an x percentage of users.

These generic guidelines or principles, along with the high-level design, help the team to make decisions at every stage.

Technology selection

Once we have an architecture, we need to define things, such as the programming languages and frameworks, and make source-versus-build choices for individual constructs. This can include database selection, vendor selection, technology strategy, deployment environment, upgrade policies, and so on. The sum of these factors can often make a straightforward task of choosing something simple into a complete nightmare. And then, finally, all of these technologies have to actually work well together.

Leadership in the kitchen

Once the team starts executing, the architect needs to provide technical leadership to the team. This does not mean taking every technical decision, but implies having ownership and ensuring that the individual components being built add up to the blueprint made. The architect sells the vision to the team at every design review meeting. Sometimes, steering needs to happen, in the form of tough questions asked to the developers in the design review (rather than prescribing solutions).

Coaching and mentoring

The developers working on such a product often need, and seek out, coaching and mentoring outside of their immediate deliverables. One of their core objectives is to learn, discuss tough problems, and improve their skills. Not having an environment where such interactions are facilitated leads to frustrations and developer churn.

While managing the technical stewardship of the product, many times, the architect needs to play the coach and mentor role for the developers. This could involve things ranging from technical feedback sessions to career counseling.

Target state versus current state

When architects and developers are given requirements, they often come up with beautiful and elegant designs. But generally, once the project kicks off, there is pressure on the team to deliver quickly. The business stakeholders want something out fast (a Minimum Viable Product), rather than wait for the Grand Final Product to be released. This makes sense in terms of de-risking the product and provides key feedback to the team, in terms of whether the product is fulfilling business requirements or not.

But this mode of operation also has a significant cost. Developers cut corners while building the project in order to meet the deadlines. Hence, even though we have a clean, beautiful target state in terms of architecture, the reality will not match this.

Having this mismatch is not wrong; rather, it's natural. But it is important for the team to have the target state in mind and define the next set of bite-sized chunks to take the product to the target state during each sprint. This means the architect needs to get involved in sprint planning for the team, along with the product and engineering managers.

Software architecture

This section briefly explores the tenants of software architecture, its relationship with design, and various architectural lenses or paradigms that are used to analyze and solve a problem.

Architecture versus design

The word is often used to refer to something at a high level that is distinct from the lower-level details, whereas design more often refers to structures and decisions at a lower level. But the two are intrinsically linked, and we cannot have a good product without synergy between the two. The low-level details and high-level structure are all part of the same whole. They form a continuous fabric that defines the shape of the system. You can't have one without the other; there is a continuum of decisions from the highest to the lowest levels.

Working separately on architecture and design, without a central theme and principles guiding both, leads to developers perceiving the code base in the same way that the blind men perceived the elephant in the famous parable.

On the other hand, it is not practical (or desirable) for the architect to document every aspect of low-level design. The key is to build a vision and guiding principles for the code, which can be used as guard rails by the developers when making decisions at each level.

What does architecture look like?

There have been multiple architectural paradigms over the year, but all of them have one key goal: managing complexity. How can we package code into components and work with these components as abstract entities to infer about and build chunks of behavior?

These components divide the system into partitions, so that each partition has a specific concern and role. Each component has well defined interfaces and responsibilities and is segregated from the rest of the components. Having this abstraction allows us to not worry about the inner workings of the components.

System decomposition needs to be a well thought-out activity. There are two key metrics for assessing how good your components are, named cohesion and coupling:

  • High cohesion means a component performs a single related task.
  • Low coupling means components should have less dependency between themselves.

A component can easily be extended to add more functionality or data to it. And, if needed, it should be completely replaceable, without that affecting the rest of the system.

Robert Cecil Martin (more commonly known as Uncle Bob) is a software engineer and author. He paints a beautiful picture through his clean architecture blog, describing the component/layering idea:

The concentric circles represent different layers (that is, different sets of components or higher-order components) of software.

In general, the inner circles are more abstract, and deal with things such as business rules and policies. They are the least likely to change when something external changes. For example, you would not expect your employee entity to change just because you want to show employee details on a mobile application, in addition to an existing web product.

The outer circles are mechanisms. They define how the inner circles are fulfilled using the mechanisms available. They are composed of things such as the database and web framework. This is generally code that you re-use, rather than write fresh.

The Controllers (or Interface Adaptors) layer converts data from the formats available in the mechanisms to what is most convenient for the business logic.

The rule that is key to making this architecture successful is the dependency rule. This rule says that source code dependencies can only point inward. Nothing in an inner circle (variables, classes, data, and private functions) can know anything at all about something in an outer circle. The interfaces between the layers and the data that crosses these boundaries are well defined and versioned. If a software system follows this rule, then any layer can be replaced or changed easily, without affecting the rest of the system.

These four layers are just indicative—different architectures will bring out different numbers and sets of layers (circles). The key is to have a logical separation of the system so that, as new code needs to be written, developers have crisp ideas on what goes where.

Here is a quick summary of main architectural paradigms that are commonly used:

Package-based

The system is broken down into packages (here, the component is the package), where each package has a well-defined purpose and interface. There is clear separation of concerns in terms of the components. However, the level of independence and enforcement of segregation between modules is variable: in some contexts, the parts have only logical separation, and a change in one component might require another component to be re-built or re-deployed.

Layering/N-tier/3-tier

This segregates functionality into separate layers, where components are hosted in a specific layer. Generally, layers only interact with the layer below, thereby reducing complexity and enabling reusability. Layers might be packages or services. The most famous example of layered architecture is the networking stack (7 layer OSI or the TCP/IP stack).

Async / message-bus / actor model / Communicating Sequential Processes (CSP)

Here, the key idea is that systems communicate with each other through messages (or events). This allows for clean decoupling: the system producing the event does not need to know about the consumers. This allows allows for 1-n communication.

In Unix, this paradigm is employed via pipes: simple tools, such as cat and grep, are coupled through pipes to enable more complex functionality such as search for cat in words.txt.

In a distributed system, the messages exist over the network. We shall look at distributed systems in detail in a later chapter. If you're wondering what the actor model or CSP is, these paradigms are explained later in this chapter.

Object-oriented

This is an architectural style where components are modeled as objects that encapsulate attributes and expose methods. The methods operate on the data within the object. This approach is discussed in detail in Chapter 3, Design Patterns.

Model-View-Controller (MVC) / separated presentation

Here, the logic for handling user interaction is placed into a view component, and the data that powers the interaction goes into a model component. The controller component orchestrates the interactions between them. We shall look at this in more detail in Chapter 6, Messaging.

Mircoservices / service-oriented architecture (SOA)

Here, the system is designed as a set of independent services that collaborate with each other to provide the necessary system behavior. Each service encapsulates its own data and has a specific purpose. The key difference here from the other paradigms is the existence of independently running and deployable services. There is a deep dive on this style further on in this chapter.

Microservices

While the theoretical concepts discussed previously have been with us for decades now, a few things have recently been changing very rapidly. There is an ever increasing amount of complexity in software products. For example, in object-oriented programming, we might start off with a clean interface between two classes, but during a sprint, under extra time pressure, a developer might cut corners and introduce a coupling between classes. Such a technical debt is rarely paid back on its own; it starts to accumulate until our initial design objective is no longer perceivable at all!

Another thing that's changing is that products are rarely built in isolation now; they make heavy use of services provided by external entities. A vivid example of this is found in managed services in cloud environments, such as Amazon Web Services (AWS). In AWS, there is a service for everything, from a database to one that enables building a chatbot.

It has become imperative that we try to enforce separation of concerns. Interactions and contracts between components are becoming increasingly Application Programming Interface (API)-driven. Components don't share memory, hence they can only communicate via network calls. Such components are called as services. A service takes requests from clients and fulfills them. Clients don't care about the internals of the service. A service can be a client for another service.

A typical initial architecture of a system is shown here:

The system can be broken into three distinct layers:

  • Frontend (a mobile application or a web page): This is what the users interact with and makes network classes go to the backend to get data and enable behavior.
  • Backend piece: This layer has the business logic for handling specific requests. This code is generally supposed to be ignorant of the frontend specifics (such as whether it is an application or a web page making the call).
  • A data store: This is the repository for persistent data.

In the early stages, when the team (or company) is young, and people start developing with a greenfield environment and the number of developers is small, things work wonderfully and there is good development velocity and quality. Developers pitch in to help other developers whenever there are any issues, since everyone knows the system components at some level, even if they're not the developer responsible for the component. However, as the company grows, the product features start to multiply, and as the team gets bigger, four significant things happen:

  • The code complexity increases exponentially and the quality starts to drop. A lot of dependencies spurt up between the current code and new features being developed, while bug fixes are made to current code. New developers don't have context into the tribal knowledge of the team and the cohesive structure of the code base starts to break.
  • Operational work (running and maintaining the application) starts taking a significant amount time for the team. This usually leads to the hiring of operational engineers (DevOps engineers) who can independently take over operations work and be on call for any issues. However, this leads to developers losing touch with production, and we often see classic issues, such as it works on my setup but fails in production.
  • The third thing that happens is the product hitting scalability limits. For example, the database may not meet the latency requirements under increased traffic. We might discover that an algorithm that was chosen for a key business rule is getting very latent. Things that were working well earlier suddenly start to fail, just because of the increased amount of data and requests.
  • Developers start writing huge amounts of tests to have quality gates. However, these regression tests become very brittle with more and more code being added. Developer productivity falls off a cliff.

Applications that are in this state are called monoliths. Sometimes, being a monolith is not bad (for example, if there are stringent performance/latency requirements), but generally, the costs of being in this state impact the product very negatively. One key idea, which has become prevalent to enable software to scale, has been microservices, and the paradigm is more generally called service-oriented architecture (SOA).

The basic concept of a microservice is simple—it's a simple, standalone application that does one thing only and does that one thing well. The objective is to retain the simplicity, isolation, and productivity of the early app. A microservice cannot live alone; no microservice is an island—it is part of a larger system, running and working alongside other microservices to accomplish what would normally be handled by one large standalone application.

Each microservice is autonomous, independent, self-contained, and individually deployable and scalable. The goal of microservice architecture is to build a system composed of such microservices.

The core difference between a monolithic application and microservices is that a monolithic application will contain all features and functions within one application (code base) deployed at the same time, with each server hosting a complete copy of the entire application, while a microservice contains only one function or feature, and lives in a microservice ecosystem along with other microservices:

Monolithic architecture

Here, there is one deployable artifact, made from one application code base that contains all of the features. Every machine runs a copy of the same code base. The database is shared and usually leads to non-explicit dependencies (Feature A requires Feature B to maintain a Table X using a specific schema, but nobody told the Feature B team!)

Contrast this with a microservices application:

Microservices-based architecture

Here, in it's canonical form, every feature is itself packaged as a service, or a microservice, to be specific. Each microservice is individually deployable and scalable and has its own separate database.

To summarize, microservices bring a lot to the table:

  • They allow us to use the componentization strategy (that is, divide and rule) more effectively, with clear boundaries between components.
  • There's the ability to create the right tool for each job in a microservice.
  • It ensures easier testability.
  • There's improved developer productivity and feature velocity.

The challenges for microservices – efficiency

A non-trivial product with microservices will have tens (if not hundreds) of microservices, all of which need to co-operate to provide higher levels of value. A challenge for this architecture is deployment—How many machines do we need?

Moore's law refers to an observation made by Intel co-founder Gordon Moore in 1965. He famously noticed that the number of transistors per square inch on integrated circuits had doubled every year since their invention, and hence, should continue to do so.

This law has more or less held true for more than 40 years now, which means that high-performance hardware has become a commodity. For many problems, throwing hardware at the problem has been an efficient solution for many companies. With cloud environments such as AWS, this is even more so the case; one can literally get more horsepower just by pressing a button:

However with the microservices paradigm, it is no longer possible to remain ignorant of efficiency or cost. Microservices would be in their tens or hundreds, with each service having multiple instances.

Besides deployment, another efficiency challenge is the developer setup—a developer needs to be able to run multiple services on their laptop in order to work on a feature. While they may be making changes in only one, they still need to run mocks/sprint-branch version of others so that one can exercise the code.

A solution that immediately comes to mind is, Can we co-host microservices on the same machine? To answer this, one of the first things to consider is the language runtime. For example, in Java, each microservice needs a separate JVM process to run, in order to enable the segregation of code. However, the JVM tends to be pretty heavy in terms of resource requirements, and worse, the resource requirements can spike, leading to one JVM process to cause others to fail due to resource hogging.

Another thing to consider about the language is the concurrency primitives. Microservices are often I/O-bound and spend a lot of time communicating with each other. Often, these interactions are parallel. If we were to use Java, then almost everything parallel needs a thread (albeit in a thread pool). Threads in Java are not lean, and typically use about 1 MB of the heap (for the stack, housekeeping data, and so on). Hence, efficient thread usage becomes an additional constraint when writing parallel code in Java. Other things to worry about include the sizing of thread pools, which degenerates into a trial-and-error exercise in many situations.

Thus, though microservices are language-agnostic, some languages are better suited and/or have better support for microservices than others. One language that stands out in terms of friendliness with microservices is Golang. It's extremely frugal with resources, lightweight, very fast, and has a fantastic support for concurrency, which is a powerful capability when running across several cores. Go also contains a very powerful standard library for writing web services for communication (as we shall see ourselves, slightly further down the line).

The challenges for microservices – programming complexity

When working in a large code base, local reasoning is extremely important. This refers to the ability of a developer to understand the behavior of a routine by examining the routine itself, rather than examining the entire system. This is an extension of what we saw previously, compartmentalization is key to managing complexity.

In a single-threaded system, when you're looking at a function that manipulates some state, you only need to read the code and understand the initial state. Isolated threads are of little use. However, when threads need to talk to each other, very risky things can happen! But by contrast, in a multi-threaded system, any arbitrary thread can possibly interfere with the execution of the function (including deep inside a library you don't even know you're using!). Hence, understanding a function means not just understanding the code in the function, but also an exhaustive cognition of all possible interactions in which the function's state can be mutated.

It's a well known fact that human beings can juggle about seven things at one time. In a big system, where there might be millions of functions and billions of possible interactions, not having local reasoning can be disastrous.

Synchronization primitives, such as mutexes and semaphores, do help, but they do come with their own baggage, including the following issues:

  • Deadlocks: Two threads requesting resources in a slightly different pattern causes both to block:
  • Priority inversion: A high priority process wait on a low-priority slow process
  • Starvation: A process occupies a resource for much more time than another equally important process

In the next section, we will see how Golang helps us to overcome these challenges and adopt microservices in the true spirit of the idea, without worrying about efficiency constraints or increased code complexity.

Go

The level of scale at Google is unprecedented. There are millions of lines of code and thousands of engineers working on it. In such an environment where there are a lot of changes done by different people, a lot of software engineering challenges will crop up—in particular, the following:

  • Code becomes hard to read and poorly documented. Contracts between components cannot be easily inferred.
  • Builds are slow. The development cycles of code-compile-test grow in difficulty, with inefficiency in modeling concurrent systems, as writing efficient code with synchronization primitives is tough.
  • Manual memory management often leads to bugs.
  • There are uncontrolled dependencies.
  • There is a variety of programming styles due to multiple ways of doing something, leading to difficulty in code reviews, among other things.

The Go programming language was conceived in late 2007 by Robert Griesemer, Rob Pike, and Ken Thompson, as an open source programming language that aims to simplify programming and make it fun again. It's sponsored by Google, but is a true open source project—it commits from Google first, to the open source projects, and then the public repository is imported internally.

The language was designed by and for people who write, read, debug, and maintain large software systems. It's a statically-typed, compiled language with built-in concurrency and garbage collection as first-class citizens. Some developers, including myself, find beauty in its minimalistic and expressive design. Others cringe at things such as a lack of generics.

Since its inception, Go has been in constant development, and already has a considerable amount of industry support. It's used in real systems in multiple web-scale applications (image source: https://madnight.github.io/githut/):

For a quick summary of what has made Go popular, you can refer to the WHY GO? section at https://smartbear.com/blog/develop/an-introduction-to-the-go-language-boldly-going-wh/.

We will now quickly recap the individual features of the language, before we start looking at how to utilize them to architect and engineer software in the rest of this book.

The following sections do not cover Go's syntax exhaustively; they are just meant as a recap. If you're very new to Go, you can take a tour of Go, available at https://tour.golang.org/welcome/1, while reading the following sections.

Hello World!

No introduction to any language is complete without the canonical Hello World program (http://en.wikipedia.org/wiki/Hello_world). This programs starts off by defining a package called main, then imports the standard Go input/output formatting package (fmt), and lastly, defines the main function, which is the standard entry point for every Go program. The main function here just outputs Hello World!:

package main

import "fmt"

func main() {
fmt.Println("Hello World!")
}

Go was designed with the explicit object of having clean, minimal code. Hence, compared to other languages in the C family, its grammar is modest in size, with about 25 keywords.

"Less is EXPONENTIALLY more."
- Robert Pike

Go statements are generally C-like, and most of the primitives should feel familiar to programmers accustomed to languages such as C and Java. This makes it easy for non-Go developers to pick up things quickly. That said, Go makes many changes to C semantics, mostly to avoid the reliability pitfalls associated with low-level resource management (that is, memory allocation, pointer arithmetic, and implicit conversions), with the aim of increasing robustness. Also, despite syntactical similarity, Go introduces many modern constructs, including concurrency and garbage collection.

Data types and structures

Go supports many elementary data types, including int, bool, int32, and float64. One of the most obvious points where the language specification diverges from the familiar C/Java syntax is where, in the declaration syntax, the declared name appears before the type. For example, consider the following snippet:

var count int

It declares a count variable of the integer type (int). When the type of a variable is unambiguous from the initial value, then Go offers a shorted variable declaration syntax pi := 3.14.

It's important to note the language is strongly typed, so the following code, for example, would not compile:

var a int = 10

var b int32 = 20

c := a + b

One unique data type in Go is the error type. It's used to store errors, and there is a helpful package called errors for working with the variables of this type:

err := errors.New("Some Error")
if err != nil {
fmt.Print(err)
}

Go, like C, gives the programmer control over pointers. For example, the following code denotes the layout of a point structure and a pointer to a Point Struct:

type Point Struct {
X, Y int
}

Go also supports compound data structures, such as string, map, array, and slice natively. The language runtime handles the details of memory management and provides the programmer with native types to work with:

var a[10]int  // an array of type [10]int

a[0] = 1 // array is 0-based

a[1] = 2 // assign value to element

var aSlice []int // slice is like an array, but without upfront sizing

var ranks map[string]int = make(map[string]int) // make allocates the map
ranks["Joe"] = 1 // set
ranks["Jane"] = 2
rankOfJoe := ranks["Joe"] // get

string s = "something"
suff := "new"
fullString := s + suff // + is concatenation for string
Go has two operators, make() and new(), which can be confusing. new() just allocates memory, whereas make() initializes structures such as map. make() hence needs to be used with maps, slices, or channels.
Slices are internally handled as Struct, with fields defining the current start of the memory extent, the current length, and the extent.

Functions and methods

As in the C/C++ world, there are code blocks called functions. They are defined by the func keyword. They have a name, some parameters, the main body of code, and optionally, a list of results. The following code block defines a function to calculate the area of a circle:

func area(radius int) float64 {
var pi float64 = 3.14
return pi*radius*radius
}

It accepts a single variable, radius, of the int type, and returns a single float64 value. Within the function, a variable called pi of the float64 type is declared.

Functions in Go can return multiple values. A common case is to return the function result and an error value as a pair, as seen in the following example:

func GetX() (x X, err error)

myX, err := GetX()
if err != nil {
...
}

Go is an object-oriented language and has concepts of structures and methods. A struct is analogous to a class and encapsulates data and related operations. For example, consider the following snippet:

type Circle struct {
Radius int
color String
}

It defines a Circle structure with two members and fields:

  • Radius, which is of the int type and is public
  • color, which is of the String type and is private
We shall look at class design and public/private visibility in more detail in Chapter 3, Design Patterns.

A method is a function with a special parameter (called a receiver), which can be passed to the function using the standard dot notation. This receiver is analogous to the self or this keyword in other languages.

Method declaration syntax places the receiver in parentheses before the function name. Here is the preceding Area function declared as a method:

func (c Circle) Area() float64 {
var pi float64 = 3.14
return pi*c.radius*c.radius
}
Receivers can either be pointers (reference) or non-pointers (value). Pointer references are useful in the same way as normal pass-by-reference variables, should you want to modify struct, or if the size of struct is large, and so on. In the previous example of Area(), the c Circle receiver is passed by value. If we passed it as c * Circle, it would be pass by reference.

Finally, on the subject of functions, it's important to note that Go has first-class functions and closures:

areaSquared := func(radius int) float64 {  
return area*area
}

There is one design decision in the function syntax that points to one of my favorite design idioms in Go—keep things explicit. With default arguments, it becomes easy to patch API contracts and overload functions. This allows for easy wins in the short term, but leads to complicated, entangled code in the long run. Go encourages developers to use separate functions, with clear names, for each such requirement. This makes the code a lot more readable. If we really need such overloading and a single function that accepts a varied number of arguments, then we can utilize Go's type-safe variadic functions.

Flow control

The main stay of flow control in code is the familiar if statement. Go has the if statement, but does not mandate parentheses for conditions. Consider the following example:

if val > 100 {
fmt.Println("val is greater than 100")
} else {
fmt.Println("val is less than or equal to 100")
}

To define loops, there is only one iteration keyword, for. There are no while or do...while keywords that we see in other languages, such as C or Java. This is in line with the Golang design principles of minimalism and simplicity—whatever we can do with a while loop, the same can be achieved with a for loop, so why have two constructs? The syntax is as follows:

func naiveSum(n Int) (int){
sum := 0;
for i:=0; i < n ; i++ {
sum += index
}
return sum
}

As you can see, again, there are no parentheses around the loop conditions. Also, the i variable is defined for the scope of the loop (with i:= 0). This syntax will be familiar to C++ or Java programmers.

Note that the for loop need not strictly follow the three-tuple initial version (declaration, check, increment). It can simply be a check, as with a while loop in other languages:

i:= 0
for i <= 2 {
fmt.Println(i)
i = i + 1
}

And finally, a while(true) statement looks like this:

for {
// forever
}

There is a range operator that allows iterations of arrays and maps. The operator is seen in action for maps here:

// range over the keys (k) and values (v) of myMAp
for k,v := range myMap {

fmt.Println("key:",k)
fmt.Println("val:",v)
}

// just range over keys
for key := range myMap {
fmt.Println("Got Key :", key)
}

The same operator works in an intuitive fashion for arrays:

    input := []int{100, 200, 300}

// iterate the array and get both the index and the element
for i, n := range input {
if n == 200 {
fmt.Println("200 is at index : ", i)
}
}
sum := 0
// in this iteration, the index is skipped, it's not needed for _, n := range input { sum += n } fmt.Println("sum:", sum)

Packaging

In Go, code is binned into packages. These packages provide a namespaces for code. Every Go source file, for instance, encoding/json/json.go, starts with a package clause, like this:

 package json

Here, json is the package name, a simple identifier. Package names are usually concise.

Packages are rarely in isolation; they have dependencies. If code in one package wants to use something from a different package, then the dependency needs to be called out explicitly. The dependent packages can be other packages from the same project, a Golang standard package, or from a third-party package on GitHub. To declare dependent packages, after the package clause, each source file may have one or more import statements, comprising the import keyword and the package identifier:

import "encoding/json”

One important design decision in Go, dependency-wise, is that the language specification requires unused dependencies to be declared as a compile-time error (not a warning, like most other build systems). If the source file imports a package it doesn't use, the program will not compile. This was done to speed up build times by making the compiler work on only those packages that are needed. For programmers, it also means that code tends to be cleaner, with less unused imports piling up. The flip side is that, if you're experimenting with different packages while coding, you may find the compiler errors irritating!

Once a package has been imported, the package name qualifies items from the package in the source file being imported:

var dec = json.NewDecoder(reader)

Go takes an unusual approach to defining the visibility of identifiers (functions/variables) inside a package. Unlike private and public keywords, in Go, the name itself carries the visibility definition. The case of the initial letter of the identifier determines the visibility. If the initial character is an uppercase letter, then the identifier is public and is exported out of the package. Such identifiers can be used outside of the package. All other identifiers are not visible (and hence not usable) outside of the host package. Consider the following snippet:

package circles

func AreaOf(c Circle) float64 {
}

func colorOf(c Circle) string {
}

In the preceding code block, the AreaOf function is exported and visible outside of the circles package, but colorOf is visible only within the package.

We shall look at packing Go code in greater detail in Chapter 3, Design Patterns.

Concurrency

Real life is concurrent. With API-driven interactions and multi-core machines, any non-trivial program written today needs to be able to schedule multiple operations in parallel, and these need to happen concurrently using the available cores. Languages such as C++ or Java did not have language-level support for concurrency for a long time. Recently, Java 8 has added support for parallelism with stream processing, but it still follows an inefficient fork-join process, and communication between parallel streams is difficult to engineer.

Communicating Sequential Processes (CSP) is a formal language for describing patterns of interaction in concurrent systems. It was first described in a 1978 paper by Tony Hoare. The key concept in CSP is that of a process. Essentially, code inside a process is sequential. At some point in time, this code can start another process. Many times, these processes need to communicate. CSP promotes the message-passing paradigm of communication, as compared to the shared memory and locks paradigm for communication. Shared memory models, like the one depicted in the following diagram, are fraught with risks:

It's easy to get deadlock and corruption if a process misbehaves or crashes inside a critical section. Such systems also experience difficulty in recovering from failure.

In contrast, CSP promotes messages passing using the concept of channels, which are essentially queues with a simple logical interface of send() and recv(). These operations can be blocking. This model is described in this following:

Go uses a variant of CSP with first-class channels. Procedures are called goroutines. Go enables code, which is mostly regular procedural code, but allows concurrent composition using independently executing functions (goroutines). In procedural programming, we can just call a function inline; however, with Go, we can also spawn a goroutine out of the function and have it execute independently.

Channels are also first-class Go primitives. Sharing is legal and passing a pointer over a channel is idiomatic (and efficient).

The main() function itself is a goroutine, and a new goroutine can be spawned using the go keyword. For example, the snippet below modifies the Hello World program to spawn a goroutine:

package main

import (
"fmt"
"time"
)

func say(what string){
fmt.Println(what)
}

func main() {
message := "Hello world!"
go say(message)
time.Sleep(5*time.Second)
}

Note that, after the go say(message) statement is executed, the main() goroutine immediately proceeds to the next statement. The time.Sleep() function is important here to prevent the program from exiting! An illustration of goroutines is shown in the following diagram:

We shall look at channels and more concurrency constructs in Chapter 4, Scaling Applications.

Garbage collection

Go has no explicit memory-freeing operation: the only way allocated memory can be returned to the pools is via garbage collection. In a concurrent system, this is an must-have feature, because the ownership of an object might change (with multiple references) in non-obvious ways. This allows programmers to concentrate on modeling and coding the concurrent aspects of the system, without having to bother about pesky resource management details. Of course, garbage collection brings in implementation complexity and latency. Nonetheless, ultimately, the language is much easier to use because of garbage collection.

Not everything thing is freed on the programmer's behalf. Sometimes, the programmer has to make explicit calls to enable the freeing of an object's memory.

Object-orientation

The Go authors felt that the normal type-hierarchy model of software development is easy to abuse. For example, consider the following class and the related description:

Coding in such large class hierarchies usually generates brittle code. Early decisions become very hard to change, and base class changes can have devastating consequences further down the line. However, the irony is that, early on, all of the requirements might not be clear, nor the system well understood enough, to allow for great base class design.

The Go way of object-orientation is : composition over inheritance.

For polymorphic behavior, Go uses interfaces and duck typing:

"If it looks like a duck and quacks like a duck, it's a duck."

Duck typing implies that any class that has all of the methods that an interfaces advertises can be said to implement the said interface.

We shall look at more detail on object-orientation in Go later on in Chapter 3, Design Patterns.

Summary

In this chapter, we looked at why having a plan is important when building big. We reviewed various design paradigms and key aspects of Golang. The discussion of these topics here was focused and very condensed. For more insights, I strongly recommend reading Clean Architecture: A Craftsman's Guide to Software Structure and Design by Robert C. Martin and A tour of Go (https://tour.golang.org/welcome/1).

In the next chapter, we will look at the problem statement of the case study that we will be working on for the rest of this book. At the end of each section of the book, we will apply whatever we learned in the section's chapters to build solutions for specific aspects of the case study.

Left arrow icon Right arrow icon
Download code icon Download Code

Key benefits

  • Gain knowledge of architectural approaches on SOA and microservices for architectural decisions
  • Explore different architectural patterns for building distributed applications
  • Migrate applications written in Java or Python to the Go language

Description

Building software requires careful planning and architectural considerations; Golang was developed with a fresh perspective on building next-generation applications on the cloud with distributed and concurrent computing concerns. Hands-On Software Architecture with Golang starts with a brief introduction to architectural elements, Go, and a case study to demonstrate architectural principles. You'll then move on to look at code-level aspects such as modularity, class design, and constructs specific to Golang and implementation of design patterns. As you make your way through the chapters, you'll explore the core objectives of architecture such as effectively managing complexity, scalability, and reliability of software systems. You'll also work through creating distributed systems and their communication before moving on to modeling and scaling of data. In the concluding chapters, you'll learn to deploy architectures and plan the migration of applications from other languages. By the end of this book, you will have gained insight into various design and architectural patterns, which will enable you to create robust, scalable architecture using Golang.

Who is this book for?

Hands-On Software Architecture with Golang is for software developers, architects, and CTOs looking to use Go in their software architecture to build enterprise-grade applications. Programming knowledge of Golang is assumed.

What you will learn

  • Understand architectural paradigms and deep dive into Microservices
  • Design parallelism/concurrency patterns and learn object-oriented design patterns in Go
  • Explore API-driven systems architecture with introduction to REST and GraphQL standards
  • Build event-driven architectures and make your architectures anti-fragile
  • Engineer scalability and learn how to migrate to Go from other languages
  • Get to grips with deployment considerations with CICD pipeline, cloud deployments, and so on
  • Build an end-to-end e-commerce (travel) application backend in Go

Product Details

Country selected
Publication date, Length, Edition, Language, ISBN-13
Publication date : Dec 07, 2018
Length: 500 pages
Edition : 1st
Language : English
ISBN-13 : 9781788625104
Vendor :
Google
Category :
Languages :

What do you get with eBook?

Product feature icon Instant access to your Digital eBook purchase
Product feature icon Download this book in EPUB and PDF formats
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want

Product Details

Publication date : Dec 07, 2018
Length: 500 pages
Edition : 1st
Language : English
ISBN-13 : 9781788625104
Vendor :
Google
Category :
Languages :

Packt Subscriptions

See our plans and pricing
Modal Close icon
$19.99 billed monthly
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Simple pricing, no contract
$199.99 billed annually
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Choose a DRM-free eBook or Video every month to keep
Feature tick icon PLUS own as many other DRM-free eBooks or Videos as you like for just $5 each
Feature tick icon Exclusive print discounts
$279.99 billed in 18 months
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Choose a DRM-free eBook or Video every month to keep
Feature tick icon PLUS own as many other DRM-free eBooks or Videos as you like for just $5 each
Feature tick icon Exclusive print discounts

Frequently bought together


Stars icon
Total $ 153.97
Hands-On Dependency Injection in Go
$43.99
Hands-On Software Architecture with Golang
$54.99
Software Architect’s Handbook
$54.99
Total $ 153.97 Stars icon

Table of Contents

13 Chapters
Building Big with Go Chevron down icon Chevron up icon
Packaging Code Chevron down icon Chevron up icon
Design Patterns Chevron down icon Chevron up icon
Scaling Applications Chevron down icon Chevron up icon
Going Distributed Chevron down icon Chevron up icon
Messaging Chevron down icon Chevron up icon
Building APIs Chevron down icon Chevron up icon
Modeling Data Chevron down icon Chevron up icon
Anti-Fragile Systems Chevron down icon Chevron up icon
Case Study – Travel Website Chevron down icon Chevron up icon
Planning for Deployment Chevron down icon Chevron up icon
Migrating Applications Chevron down icon Chevron up icon
Other Books You May Enjoy Chevron down icon Chevron up icon

Customer reviews

Most Recent
Rating distribution
Full star icon Full star icon Full star icon Full star icon Empty star icon 4
(12 Ratings)
5 star 58.3%
4 star 16.7%
3 star 0%
2 star 16.7%
1 star 8.3%
Filter icon Filter
Most Recent

Filter reviews by




Rich May 09, 2023
Full star icon Full star icon Empty star icon Empty star icon Empty star icon 2
I think the breadth of the content is great. The print is lacking… I’ve uploaded some examples. Graphs that are meant to be differentiated are the same color. N^2 is n2. The publishing company and editor could have done a better job… or this print is a fake book being sold on Amazon for full price.
Amazon Verified review Amazon
Kindle Customer Aug 08, 2021
Full star icon Full star icon Full star icon Full star icon Full star icon 5
There were DOZENS of small grammatical errors, to the point that I stopped marking them and just inferred what the author meant. If they'd like to get back to me I can tell them the pages where the english grammar was incorrect. Overall a very enlightening book on distributed systems architecture with GOLang and a well written great book overall.
Amazon Verified review Amazon
Daniel Miranda Jul 25, 2021
Full star icon Full star icon Full star icon Full star icon Empty star icon 4
Great book for non advanced users, it covers a lot of important concepts, but some examples are a little bit shallow.
Amazon Verified review Amazon
Diego Neves May 09, 2020
Full star icon Full star icon Full star icon Full star icon Empty star icon 4
i liked this book, It's came various aspect in the architeture of sotware, but, in my opinion, your knowloged in the microsservice architeture is much better introduced
Amazon Verified review Amazon
shailyn ortiz Jul 12, 2019
Full star icon Full star icon Full star icon Full star icon Full star icon 5
Best book on software architectures, that is its main focus, not to teach go itself. If you are looking for a go book try go programming blueprints
Amazon Verified review Amazon
Get free access to Packt library with over 7500+ books and video courses for 7 days!
Start Free Trial

FAQs

How do I buy and download an eBook? Chevron down icon Chevron up icon

Where there is an eBook version of a title available, you can buy it from the book details for that title. Add either the standalone eBook or the eBook and print book bundle to your shopping cart. Your eBook will show in your cart as a product on its own. After completing checkout and payment in the normal way, you will receive your receipt on the screen containing a link to a personalised PDF download file. This link will remain active for 30 days. You can download backup copies of the file by logging in to your account at any time.

If you already have Adobe reader installed, then clicking on the link will download and open the PDF file directly. If you don't, then save the PDF file on your machine and download the Reader to view it.

Please Note: Packt eBooks are non-returnable and non-refundable.

Packt eBook and Licensing When you buy an eBook from Packt Publishing, completing your purchase means you accept the terms of our licence agreement. Please read the full text of the agreement. In it we have tried to balance the need for the ebook to be usable for you the reader with our needs to protect the rights of us as Publishers and of our authors. In summary, the agreement says:

  • You may make copies of your eBook for your own use onto any machine
  • You may not pass copies of the eBook on to anyone else
How can I make a purchase on your website? Chevron down icon Chevron up icon

If you want to purchase a video course, eBook or Bundle (Print+eBook) please follow below steps:

  1. Register on our website using your email address and the password.
  2. Search for the title by name or ISBN using the search option.
  3. Select the title you want to purchase.
  4. Choose the format you wish to purchase the title in; if you order the Print Book, you get a free eBook copy of the same title. 
  5. Proceed with the checkout process (payment to be made using Credit Card, Debit Cart, or PayPal)
Where can I access support around an eBook? Chevron down icon Chevron up icon
  • If you experience a problem with using or installing Adobe Reader, the contact Adobe directly.
  • To view the errata for the book, see www.packtpub.com/support and view the pages for the title you have.
  • To view your account details or to download a new copy of the book go to www.packtpub.com/account
  • To contact us directly if a problem is not resolved, use www.packtpub.com/contact-us
What eBook formats do Packt support? Chevron down icon Chevron up icon

Our eBooks are currently available in a variety of formats such as PDF and ePubs. In the future, this may well change with trends and development in technology, but please note that our PDFs are not Adobe eBook Reader format, which has greater restrictions on security.

You will need to use Adobe Reader v9 or later in order to read Packt's PDF eBooks.

What are the benefits of eBooks? Chevron down icon Chevron up icon
  • You can get the information you need immediately
  • You can easily take them with you on a laptop
  • You can download them an unlimited number of times
  • You can print them out
  • They are copy-paste enabled
  • They are searchable
  • There is no password protection
  • They are lower price than print
  • They save resources and space
What is an eBook? Chevron down icon Chevron up icon

Packt eBooks are a complete electronic version of the print edition, available in PDF and ePub formats. Every piece of content down to the page numbering is the same. Because we save the costs of printing and shipping the book to you, we are able to offer eBooks at a lower cost than print editions.

When you have purchased an eBook, simply login to your account and click on the link in Your Download Area. We recommend you saving the file to your hard drive before opening it.

For optimal viewing of our eBooks, we recommend you download and install the free Adobe Reader version 9.