Go interfaces are powerful tools for designing flexible and adaptable code. However, their inner workings can often seem hidden behind the simple syntax.
This blog post aims to peel back the layers and explore the internals of Go interfaces, providing you with a deeper understanding of their power and capabilities.
1. Interfaces: Not Just Method Signatures
While interfaces appear as collections of method signatures, they are deeper than that. An interface defines a contract: any type that implements the interface guarantees the ability to perform specific actions through those methods. This contract-based approach promotes loose coupling and enhances code reusability.
// Interface defining a "printable" behaviortypePrintable interface { String() string}// Struct types implementing the Printable interfacetypeBook struct { Title string}typeArticle struct { Title string Content string}// Implement String() method to fulfill the contractfunc (b Book) String() string {return b.Title}// Implement String() method to fulfill the contractfunc (a Article) String() string {return fmt.Sprintf("%s", a.Title)}
Here, both Book and Article types implement the Printable interface by providing a String() method. This allows us to treat them interchangeably in functions expecting Printable values.
2. Interface Values and Dynamic Typing
An interface variable itself cannot hold a value. Instead, it refers to an underlying concrete type that implements the interface. Go uses dynamic typing to determine the actual type at runtime. This allows for flexible operations like:
func printAll(printables []Printable) { for _, p := range printables { fmt.Println(p.String()) // Calls the appropriate String() based on concrete type }}book := Book{Title: "Go for Beginners"}article := Article{Title: "The power of interfaces"}printables := []Printable{book, article}printAll(printables)
The printAll function takes a slice of Printable and iterates over it. Go dynamically invokes the correct String() method based on the concrete type of each element (Book or Article) within the slice.
3. Embedded Interfaces and Interface Inheritance
Go interfaces support embedding existing interfaces to create more complex contracts. This allows for code reuse and hierarchical relationships, further enhancing the flexibility of your code:
typeWriter interface { Write(data []byte) (int, error)}typeReadWriter interface { Writer Read([]byte) (int, error)}typeMyFile struct {// ... file data and methods}// MyFile implements both Writer and ReadWriter by embedding their interfacesfunc (f *MyFile) Write(data []byte) (int, error) {// ... write data to file}func (f *MyFile) Read(data []byte) (int, error) {// ... read data from file}
Here, ReadWriter inherits all methods from the embedded Writer interface, effectively creating a more specific “read-write” contract.
4. The Empty Interface and Its Power
The special interface{} represents the empty interface, meaning it requires no specific methods. This seemingly simple concept unlocks powerful capabilities:
// Function accepting any type using the empty interfacefunc PrintAnything(value interface{}) { fmt.Println(reflect.TypeOf(value), value)}PrintAnything(42) // Output: int 42PrintAnything("Hello") // Output: string HelloPrintAnything(MyFile{}) // Output: main.MyFile {}
This function can accept any type because interface{} has no requirements. Internally, Go uses reflection to extract the actual type and value at runtime, enabling generic operations.
5. Understanding Interface Equality and Comparisons
Equality checks on interface values involve both the dynamic type and underlying value:
book1 := Book{Title: "Go for Beginners"}book2 := Book{Title: "Go for Beginners"}// Same type and value, so equalfmt.Println(book1 == book2) // TruedifferentBook := Book{Title: "Go for Dummies"}// Same type, different value, so not equalfmt.Println(book1 == differentBook) // Falsearticle := Article{Title: "Go for Beginners"}// This will cause a compilation errorfmt.Println(book1 == article) // Error: invalid operation: book1 == article (mismatched types Book and Article)
However, it’s essential to remember that interfaces themselves cannot be directly compared using the == operator unless they both contain exactly the same value of the same type.
To compare interface values effectively, you can utilize two main approaches:
1. Type Assertions: These allow you to safely access the underlying value and perform comparisons if you’re certain about the actual type:
func getBookTitleFromPrintable(p Printable) (string, bool) { book, ok := p.(Book) // Check if p is a Bookif ok {return book.Title, true }return"", false// Return empty string and false if not a Book}bookTitle, ok :=getBookTitleFromPrintable(article)if ok { fmt.Println("Extracted book title:", bookTitle)} else { fmt.Println("Article is not a Book")}
2. Custom Comparison Functions: You can also create dedicated functions to compare interface values based on specific criteria:
The Increment method receives a pointer to MyCounter, allowing it to directly modify the count field.
7. Error Handling and Interfaces
Go interfaces play a crucial role in error handling. The built-in error interface defines a single method, Error() string, used to represent errors:
typeerror interface { Error() string}// Custom error type implementing the error interfacetypeMyError struct { message string}func (e MyError) Error() string {return e.message}func myFunction() error {// ... some operationreturn MyError{"Something went wrong"}}iferr :=myFunction(); err != nil { fmt.Println("Error:", err.Error()) // Prints "Something went wrong"}
By adhering to the error interface, custom errors can be seamlessly integrated into Go’s error-handling mechanisms.
8. Interface Values and Nil
Interface values can be nil, indicating they don’t hold any concrete value. However, attempting to call methods on a nil interface value results in a panic.
var printable Printable // nil interface valuefmt.Println(printable.String()) // Panics!
Always check for nil before calling methods on interface values.
However, it’s important to understand that an interface{} value doesn’t simply hold a reference to the underlying data. Internally, Go creates a special structure to store both the type information and the actual value. This hidden structure is often referred to as “boxing” the value.
Imagine a small container holding both a label indicating the type (e.g., int, string) and the actual data inside something like this:
typeiface struct { tab *itab data unsafe.Pointer}
Technically, this structure involves two components:
tab: This type descriptor carries details like the interface’s method set, the underlying type, and the methods of the underlying type that implement the interface.
data pointer: This pointer directly points to the memory location where the actual value resides.
When you retrieve a value from an interface{}, Go performs “unboxing.” It reads the type information and data pointer and then creates a new variable of the appropriate type based on this information.
This internal mechanism might seem complex, but the Go runtime handles it seamlessly. However, understanding this concept can give you deeper insights into how Go interfaces work under the hood.
9. Conclusion
This journey through the magic of Go interfaces has hopefully provided you with a deeper understanding of their capabilities and how they work. We’ve explored how they go beyond simple method signatures to define contracts, enable dynamic behavior, and making it way more flexible.
Remember, interfaces are not just tools for code reuse, but also powerful mechanisms for designing adaptable and maintainable applications.
Here are some key takeaways to keep in mind:
Interfaces define contracts, not just method signatures.
Interfaces enable dynamic typing and flexible operations.
Embedded interfaces allow for hierarchical relationships and code reuse.
The empty interface unlocks powerful generic capabilities.
Understand the nuances of interface equality and comparisons.
Interfaces play a crucial role in Go’s error-handling mechanisms.
Be mindful of nil interface values and potential panics.
In the first, getting started with Kubernetes operators (Helm based), and the second part, getting started with Kubernetes operators (Ansible based), of this Introduction to Kubernetes operators blog series we learned various concepts related to Kubernetes operators and created a Helm based operator and an Ansible based operator respectively. In this final part, we will build a Golang based operator. In case of Helm based operators, we were executing a helm chart when changes were made to the custom object type of our application, similarly in the case of an Ansible based operator we executed an Ansible role. In case of Golang based operator we write the code for the action we need to perform (reconcile logic) whenever the state of our custom object change, this makes the Golang based operators quite powerful and flexible, at the same time making them the most complex to build out of the 3 types.
What Will We Build?
The database server we deployed as part of our book store app in previous blogs didn’t have any persistent volume attached to it and we would lose data in case the pod restarts, to avoid this we will attach a persistent volume attached to the host (K8s worker nodes ) and run our database as an statefulset rather than a deployment. We will also add a feature to expand the persistent volume associated with the mongodb pod.
Building the Operator
1. Set up the project:
operator-sdk new bookstore-operator –dep-manager=dep
INFO[0000] Generating api version blog.velotio.com/v1alpha1 for kind BookStore. INFO[0000] Created pkg/apis/blog/group.go INFO[0001] Created pkg/apis/blog/v1alpha1/bookstore_types.go INFO[0001] Created pkg/apis/addtoscheme_blog_v1alpha1.go INFO[0001] Created pkg/apis/blog/v1alpha1/register.go INFO[0001] Created pkg/apis/blog/v1alpha1/doc.go INFO[0001] Created deploy/crds/blog.velotio.com_v1alpha1_bookstore_cr.yaml INFO[0009] Created deploy/crds/blog.velotio.com_bookstores_crd.yaml INFO[0009] Running deepcopy code-generation for Custom Resource group versions: [blog:[v1alpha1], ] INFO[0010] Code-generation complete. INFO[0010] Running OpenAPI code-generation for Custom Resource group versions: [blog:[v1alpha1], ] INFO[0011] Created deploy/crds/blog.velotio.com_bookstores_crd.yaml INFO[0011] Code-generation complete. INFO[0011] API generation complete.
The above command creates the bookstore-operator folder in our $GOPATH/src, here we have set the –dep-manager as dep which signifies we want to use dep for managing dependencies, by default it uses go modules for managing dependencies. Similar to what we have seen earlier the operator sdk creates all the necessary folder structure for us inside the bookstore-operator folder.
2. Add the custom resource definition
operator-sdk add api –api-version=blog.velotio.com/v1alpha1 –kind=BookStore
The above command creates the CRD and CR for the BookStore type. It also creates the golang structs (pkg/apis/blog/v1alpha1/bookstore_types.go) for BookStore types. It also registers the custom type (pkg/apis/blog/v1alpha1/register.go) with schema and generates deep-copy methods as well. Here we can see that all the generic tasks are being done by the operator framework itself allowing us to focus on building and object and the controller. We will update the spec of our BookStore object later. We will update the spec of BookStore type to include two custom types BookApp and BookDB.
INFO[0000] Generating controller version blog.velotio.com/v1alpha1 for kind BookStore. INFO[0000] Created pkg/controller/bookstore/bookstore_controller.go INFO[0000] Created pkg/controller/add_bookstore.go INFO[0000] Controller generation complete.
The above command adds the bookstore controller (pkg/controller/bookstore/bookstore_controller.go) to the project and also adds it to the manager.
If we take a look at the add function in the bookstore_controller.go file we can see that a new controller is created here and added to the manager so that the manager can start the controller when it (manager) comes up, the add(mgr manager.Manager, r reconcile.Reconciler)is called by the public function Add(mgr manager.Manager)which also creates a new reconciler objects and passes it to the addwhere the controller is associated with the reconciler, in the addfunction we also set the type of object (BookStore) which the controller will watch.
// Watch for changes to primary resource BookStore err = c.Watch(&source.Kind{Type: &blogv1alpha1.BookStore{}}, &handler.EnqueueRequestForObject{})if err != nil {return err }
This ensures that for any events related to any object of BookStore type, a reconcile request (a namespace/name key) is sent to the Reconcilemethod associated with the reconciler object (ReconcileBookStore) here.
4. Build the reconcile logic
The reconcile logic is implemented inside the Reconcilemethod of the reconciler object of the custom type which implements the reconcile loop.
As a part of our reconcile logic we will do the following
Create the bookstore app deployment if it doesn’t exist.
Create the bookstore app service if it doesn’t exist.
Create the Mongodb statefulset if it doesn’t exist.
Create the Mongodb service if it doesn’t exist.
Ensure deployments and services match their desired configurations like the replica count, image tag, service port, size of the PV associated with the Mongodb statefulset etc.
There are three possible events that can happen with the BookStore object
The object got created: Whenever an object of kind BookStore is created we create all the k8s resources we mentioned above
The object has been updated: When the object gets updated then we update all the k8s resources associated with it..
The object has been deleted: When the object gets deleted we don’t need to do anything as while creating the K8s objects we will set the `BookStore` type as its owner which will ensure that all the K8s objects associated with it gets automatically deleted when we delete the object.
On receiving the reconcile request the first step if to lookup for the object.
If the object is not found, we assume that it got deleted and don’t requeue the request considering the reconcile to be successful.
If any error occurs while doing the reconcile then we return the error and whenever we return non nil error value then controller requeues the request.
In the reconcile logic we call the BookStore method which creates or updates all the k8s objects associated with the BookStore objects based on whether the object has been created or updated.
The implementation of the above method is a bit hacky but gives an idea of the flow. In the above function, we can see that we are setting the BookStore type as an owner for all the resources controllerutil.SetControllerReference(c, bookStoreDep, r.scheme) as we had discussed earlier. If we look at the owner reference for these objects we would see something like this.
The approach to deploy and verify the working of the bookstore application is similar to what we did in the previous two blogs the only difference being that now we have deployed the Mongodb as a stateful set and even if we restart the pod we will see that the information that we stored will still be available.
6. Verify volume expansion
For updatingthe volume associated with the mongodb instance we first need to update the size of the volume we specified while creating the bookstore object. In the example above I had set it to 2GB let’s update it to 3GB and update the bookstore object.
Once the bookstore object is updated if we describe the mongodb PVC we will see that it still has 2GB PV but the conditions we will see something like this.
Conditions: Type Status LastProbeTime LastTransitionTime Reason Message---------------------------------------------------------- FileSystemResizePending True Mon, 01 Jan 000100:00:00+0000 Mon, 30 Sep 201915:07:01+0530 Waiting for user to (re-)start a pod to finish file system resize of volume on node.@velotiotech
It is clear from the message that we need to restart the pod for resizing of volume to reflect. Once we delete the pod it will get restarted and the PVC will get updated to reflect the expanded volume size.
Golang based operators are built mostly for stateful applications like databases. The operator can automate complex operational tasks allow us to run applications with ease. At the same time, building and maintaining it can be quite complex and we should build one only when we are fully convinced that our requirements can’t be met with any other type of operator. Operators are an interesting and emerging area in Kubernetes and I hope this blog series on getting started with it help the readers in learning the basics of it.
There are some new players in town for server programming and this time it’s all about Google. Golang has rapidly been gaining popularity ever since Google started using it for their own production systems. And since the inception of Microservice Architecture, people have been focusing on modern data communication solutions like gRPC along with Protobuf. In this post, I will walk you through each of these briefly.
Golang
Golang or Go is an open source, general purpose programming language by Google. It has been gaining popularity recently for all the good reasons. It may come as a surprise to most people that language is almost 10 years old and has been production ready for almost 7 years, according to Google.
Golang is designed to be simple, modern, easy to understand, and quick to grasp. The creators of the language designed it in such a way that an average programmer can have a working knowledge of the language over a weekend. I can attest to the fact that they definitely succeeded. Speaking of the creators, these are the experts that have been involved in the original draft of the C language so we can be assured that these guys know what they are doing.
That’s all good but why do we need another language?
For most of the use cases, we actually don’t. In fact, Go doesn’t solve any new problems that haven’t been solved by some other language/tool before. But it does try to solve a specific set of relevant problems that people generally face in an efficient, elegant, and intuitive manner. Go’s primary focus is the following:
First class support for concurrency
An elegant, modern language that is very simple to its core
Very good performance
First hand support for the tools required for modern software development
I’m going to briefly explain how Go provides all of the above. You can read more about the language and its features in detail from Go’s official website.
Concurrency
Concurrency is one of the primary concerns in most of the server applications and it should be the primary concern of the language, considering the modern microprocessors. Go introduces a concept called a ‘goroutine’. A ‘goroutine’ is analogous to a ‘lightweight user-space thread’. It is much more complicated than that in reality as several goroutines multiplex on a single thread but the above expression should give you a general idea. These are light enough that you can actually spin up a million goroutines simultaneously as they start with a very tiny stack. In fact, that’s recommended. Any function/method in Go can be used to spawn a Goroutine. You can just do ‘go myAsyncTask()’ to spawn a goroutine from ‘myAsyncTask’ function. The following is an example:
// This function performs the given task concurrently by spawing a goroutine// for each of those tasks.func performAsyncTasks(task []Task) { for _, task := range tasks {// This will spawn a separate goroutine to carry out this task.// This call is non-blocking go task.Execute() }}
Yes, it’s that easy and it is meant to be that way as Go is a simple language and you are expected to spawn a goroutine for every independent async task without caring much. Go’s runtime automatically takes care of running the goroutines in parallel if multiple cores are available. But how do these goroutines communicate? The answer is channels.
‘Channel’ is also a language primitive that is meant to be used for communication among goroutines. You can pass anything from a channel to another goroutine (A primitive Go type or a Go struct or even other channels). A channel is essentially a blocking double ended queue (can be single ended too). If you want a goroutine(s) to wait for a certain condition to be met before continuing further you can implement cooperative blocking of goroutines with the help of channels.
These two primitives give a lot of flexibility and simplicity in writing asynchronous or parallel code. Other helper libraries like a goroutine pool can be easily created from the above primitives. One basic example is:
package executorimport ("log""sync/atomic")// The Executor struct is the main executor for tasks.// 'maxWorkers' represents the maximum number of simultaneous goroutines.// 'ActiveWorkers' tells the number of active goroutines spawned by the Executor at given time.// 'Tasks' is the channel on which the Executor receives the tasks.// 'Reports' is channel on which the Executor publishes the every tasks reports.// 'signals' is channel that can be used to control the executor. Right now, only the termination// signal is supported which is essentially is sending '1' on this channel by the client.typeExecutor struct { maxWorkers int64 ActiveWorkers int64 Tasks chan Task Reports chan Report signals chan int}// NewExecutor creates a new Executor.// 'maxWorkers' tells the maximum number of simultaneous goroutines.// 'signals' channel can be used to control the Executor.func NewExecutor(maxWorkers int, signals chan int) *Executor {chanSize :=1000if maxWorkers > chanSize { chanSize = maxWorkers }executor := Executor{maxWorkers: int64(maxWorkers),Tasks: make(chan Task, chanSize),Reports: make(chan Report, chanSize),signals: signals, } go executor.launch()return&executor}// launch starts the main loop for polling on the all the relevant channels and handling differents// messages.func (executor *Executor) launch() int {reports :=make(chan Report, executor.maxWorkers) for { select {casesignal :=<-executor.signals:if executor.handleSignals(signal) ==0 {return0 }caser :=<-reports: executor.addReport(r)default:if executor.ActiveWorkers < executor.maxWorkers &&len(executor.Tasks) >0 {task :=<-executor.Tasks atomic.AddInt64(&executor.ActiveWorkers, 1) go executor.launchWorker(task, reports) } } }}// handleSignals is called whenever anything is received on the 'signals' channel.// It performs the relevant task according to the received signal(request) and then responds either// with 0 or 1 indicating whether the request was respected(0) or rejected(1).func (executor *Executor) handleSignals(signal int) int {if signal ==1 { log.Println("Received termination request...")if executor.Inactive() { log.Println("No active workers, exiting...") executor.signals <-0return0 } executor.signals <-1 log.Println("Some tasks are still active...") }return1}// launchWorker is called whenever a new Task is received and Executor can spawn more workers to spawn// a new Worker.// Each worker is launched on a new goroutine. It performs the given task and publishes the report on// the Executor's internal reports channel.func (executor *Executor) launchWorker(task Task, reports chan<- Report) {report := task.Execute()iflen(reports) <cap(reports) { reports <- report } else { log.Println("Executor's report channel is full...") } atomic.AddInt64(&executor.ActiveWorkers, -1)}// AddTask is used to submit a new task to the Executor is a non-blocking way. The Client can submit// a new task using the Executor's tasks channel directly but that will block if the tasks channel is// full.// It should be considered that this method doesn't add the given task if the tasks channel is full// and it is up to client to try again later.func (executor *Executor) AddTask(task Task) bool {iflen(executor.Tasks) ==cap(executor.Tasks) {returnfalse } executor.Tasks <- taskreturntrue}// addReport is used by the Executor to publish the reports in a non-blocking way. It client is not// reading the reports channel or is slower that the Executor publishing the reports, the Executor's// reports channel is going to get full. In that case this method will not block and that report will// not be added.func (executor *Executor) addReport(report Report) bool {iflen(executor.Reports) ==cap(executor.Reports) {returnfalse } executor.Reports <- reportreturntrue}// Inactive checks if the Executor is idle. This happens when there are no pending tasks, active// workers and reports to publish.func (executor *Executor) Inactive() bool {return executor.ActiveWorkers ==0&&len(executor.Tasks) ==0&&len(executor.Reports) ==0}
Simple Language
Unlike a lot of other modern languages, Golang doesn’t have a lot of features. In fact, a compelling case can be made for the language being too restrictive in its feature set and that’s intended. It is not designed around a programming paradigm like Java or designed to support multiple programming paradigms like Python. It’s just bare bones structural programming. Just the essential features thrown into the language and not a single thing more.
After looking at the language, you may feel that the language doesn’t follow any particular philosophy or direction and it feels like every feature is included in here to solve a specific problem and nothing more than that. For example, it has methods and interfaces but not classes; the compiler produces a statically linked binary but still has a garbage collector; it has strict static typing but doesn’t support generics. The language does have a thin runtime but doesn’t support exceptions.
The main idea here that the developer should spend the least amount of time expressing his/her idea or algorithm as code without thinking about “What’s the best way to do this in x language?” and it should be easy to understand for others. It’s still not perfect, it does feel limiting from time to time and some of the essential features like Generics and Exceptions are being considered for the ‘Go 2’.
Performance
Single threaded execution performance NOT a good metric to judge a language, especially when the language is focused around concurrency and parallelism. But still, Golang sports impressive benchmark numbers only beaten by hardcore system programming languages like C, C++, Rust, etc. and it is still improving. The performance is actually very impressive considering its a Garbage collected language and is good enough for almost every use case.
The adoption of a new tool/language directly depends on its developer experience. And the adoption of Go does speak for its tooling. Here we can see that same ideas and tooling is very minimal but sufficient. It’s all achieved by the ‘go’ command and its subcommands. It’s all command line.
There is no package manager for the language like pip, npm. But you can get any community package by just doing
go get github.com/velotiotech/WebCrawler/blob/master/executor/executor.go
Yes, it works. You can just pull packages directly from github or anywhere else. They are just source files.
But what about package.json..? I don’t see any equivalent for `go get`. Because there isn’t. You don’t need to specify all your dependency in a single file. You can directly use:
import"github.com/xlab/pocketsphinx-go/sphinx"
In your source file itself and when you do `go build` it will automatically `go get` it for you. You can see the full source file here:
This binds the dependency declaration with source itself.
As you can see by now, it’s simple, minimal and yet sufficient and elegant. There is first hand support for both unit tests and benchmarks with flame charts too. Just like the feature set, it also has its downsides. For example, `go get` doesn’t support versions and you are locked to the import URL passed in you source file. It is evolving and other tools have come up for dependency management.
Golang was originally designed to solve the problems that Google had with their massive code bases and the imperative need to code efficient concurrent apps. It makes coding applications/libraries that utilize the multicore nature of modern microchips very easy. And, it never gets into a developer’s way. It’s a simple modern language and it never tries to become anything more that that.
Protobuf (Protocol Buffers)
Protobuf or Protocol Buffers is a binary communication format by Google. It is used to serialize structured data. A communication format? Kind of like JSON? Yes. It’s more than 10 years old and Google has been using it for a while now.
But don’t we have JSON and it’s so ubiquitous…
Just like Golang, Protobufs doesn’t really solve anything new. It just solves existing problems more efficiently and in a modern way. Unlike Golang, they are not necessarily more elegant than the existing solutions. Here are the focus points of protobuf:
It’s a binary format, unlike JSON and XML, which are text based and hence it’s vastly space efficient.
First hand and sophisticated support for schemas.
First hand support for generating parsing and consumer code in various languages.
Binary format and speed
So are protobuf really that fast? The short answer is, yes. According to the Google Developers they are 3 to 10 times smaller and 20 to 100 times faster than XML. It’s not a surprise as it is a binary format, the serialized data is not human readable.
Protobufs take a more planned approach. You define `.proto` files which are kind of the schema files but are much more powerful. You essentially define how you want your messages to be structured, which fields are optional or required, their data types etc. After that the protobuf compiler will generate the data access classes for you. You can use these classes in your business logic to facilitate communication.
Looking at a `.proto` file related to a service will also give you a very clear idea of the specifics of the communication and the features that are exposed. A typical .proto file looks like this:
message Person { required string name =1; required int32 id =2; optional string email =3;enumPhoneType {MOBILE=0;HOME=1;WORK=2; } message PhoneNumber { required string number =1; optional PhoneType type =2 [default =HOME]; } repeated PhoneNumber phone =4;}
Fun Fact: Jon Skeet, the king of Stack Overflow is one of the main contributors in the project.
gRPC
gRPC, as you guessed it, is a modern RPC (Remote Procedure Call) framework. It is a batteries included framework with built in support for load balancing, tracing, health checking, and authentication. It was open sourced by Google in 2015 and it’s been gaining popularity ever since.
An RPC framework…? What about REST…?
SOAP with WSDL has been used long time for communication between different systems in a Service Oriented Architecture. At the time, the contracts used to be strictly defined and systems were big and monolithic, exposing a large number of such interfaces.
Then came the concept of ‘browsing’ where the server and client don’t need to be tightly coupled. A client should be able to browse service offerings even if they were coded independently. If the client demanded the information about a book, the service along with what’s requested may also offer a list of related books so that client can browse. REST paradigm was essential to this as it allows the server and client to communicate freely without strict restriction using some primitive verbs.
As you can see above, the service is behaving like a monolithic system, which along with what is required is also doing n number of other things to provide the client with the intended `browsing` experience. But this is not always the use case. Is it?
Enter the Microservices
There are many reasons to adopt for a Microservice Architecture. The prominent one being the fact that it is very hard to scale a Monolithic system. While designing a big system with Microservices Architecture each business or technical requirement is intended to be carried out as a cooperative composition of several primitive ‘micro’ services.
These services don’t need to be comprehensive in their responses. They should perform specific duties with expected responses. Ideally, they should behave like pure functions for seamless composability.
Now using REST as a communication paradigm for such services doesn’t provide us with much of a benefit. However, exposing a REST API for a service does enable a lot of expression capability for that service but again if such expression power is neither required nor intended we can use a paradigm that focuses more on other factors.
gRPC intends to improve upon the following technical aspects over traditional HTTP requests:
HTTP/2 by default with all its goodies.
Protobuf as machines are talking.
Dedicated support for streaming calls thanks to HTTP/2.
Pluggable auth, tracing, load balancing and health checking because you always need these.
As it’s an RPC framework, we again have concepts like Service Definition and Interface Description Language which may feel alien to the people who were not there before REST but this time it feels a lot less clumsy as gRPC uses Protobuf for both of these.
Protobuf is designed in such a way that it can be used as a communication format as well as a protocol specification tool without introducing anything new. A typical gRPC service definition looks like this:
You just write a `.proto` file for your service describing the interface name, what it expects, and what it returns as Protobuf messages. Protobuf compiler will then generate both the client and server side code. Clients can call this directly and server-side can implement these APIs to fill in the business logic.
Conclusion
Golang, along with gRPC using Protobuf is an emerging stack for modern server programming. Golang simplifies making concurrent/parallel applications and gRPC with Protobuf enables efficient communication with a pleasing developer experience.
We live in a world where speed is important. With cutting-edge technology coming into the telecommunications and software industry, we expect to get things done quickly. We want to develop applications that are fast, can process high volumes of data and requests, and keep the end-user happy.
This is great, but of course, it’s easier said than done. That’s why concurrency and parallelism are important in application development. We must process data as fast as possible. Every programming language has its own way of dealing with this, and we will see how Golang does it.
Now, many of us choose Golang because of its concurrency, and the inclusion of goroutines and channels has massively impacted the concurrency.
This blog will cover channels and how they work internally, as well as their key components. To benefit the most from this content, it will help to know a little about goroutines and channels as this blog gets into the internals of channels. If you don’t know anything, then don’t worry, we’ll be starting off with an introduction to channels, and then we’ll see how they operate.
What are channels?
Normally, when we talk about channels, we think of the ones in applications like RabbitMQ, Redis, AWS SQS, and so on. Anyone with no or only a small amount of Golang knowledge would think like this. But Channels in Golang are different from a work queue system. In the work queue system like above, there are TCP connections to the channels, but in Go, the channel is a data structure or even a design pattern, which we’ll explain later. So, what are the channels in Golang exactly?
Channels are the medium through which goroutines can communicate with each other. In simple terms, a channel is a pipe that allows a goroutine to either put or read the data.
What are goroutines?
So, a channel is a communication medium for goroutines. Now, let’s give a quick overview of what goroutines are. If you know this already, feel free to skip this section.
Technically, a goroutine is a function that executes independently in a concurrent fashion. In simple terms, it’s a lightweight thread that’s managed by go runtime.
You can create a goroutine by using a Go keyword before a function call.
Let’s say there’s a function called PrintHello, like this:
func PrintHello() { fmt.Println("Hello")}
You can make this into a goroutine simply by calling this function, as below:
//create goroutine go PrintHello()
Now, let’s head back to channels, as that’s the important topic of this blog.
How to define a channel?
Let’s see a syntax that will declare a channel. We can do so by using the chan keyword provided by Go.
You must specify the data type as the channel can handle data of the same data type.
//create channelvar c chan int
Very simple! But this is not useful since it would create a Nil channel. Let’s print it and see.
fmt.Println(c)fmt.Printf("Type of channel: %T", c)<nil>Type of channel: chan int
As you can see, we have just declared the channel, but we can’t transport data through it. So, to create a useful channel, we must use the make function.
//create channelc :=make(chan int)fmt.Printf("Type of `c`: %T\n", c)fmt.Printf("Value of `c` is %v\n", c)Type of`c`: chan intValue of`c` is 0xc000022120
As you may notice here, the value of c is a memory address. Keep in mind that channels are nothing but pointers. That’s why we can pass them to goroutines, and we can easily put the data or read the data. Now, let’s quickly see how to read and write the data to a channel.
Read and write operations on a channel:
Go provides an easy way to read and write data to a channel by using the left arrow.
c <-10
This is a simple syntax to put the value in our created channel. The same syntax is used to define the “send” only type of channels.
And to get/read the data from channel, we do this:
<-c
This is also the way to define the “receive” only type of channels.
Let’s see a simple program to use the channels.
func printChannelData(c chan int) { fmt.Println("Data in channel is: ", <-c)}
This simple function just prints whatever data is in the channel. Now, let’s see the main function that will push the data into the channel.
func main() { fmt.Println("Main started...")//create channel of intc :=make(chan int)// call to goroutine go printChannelData(c)// put the data in channel c <-10 fmt.Println("Main ended...")}
This yields to the output:
Main started...Data in channel is: 10Main ended...
Let’s talk about the execution of the program.
1. We declared a printChannelData function, which accepts a channel c of data type integer. In this function, we are just reading data from channel c and printing it.
2. Now, this method will first print “main started…” to the console.
3. Then, we have created the channel c of data type integer using the make keyword.
4. We now pass the channel to the function printChannelData, and as we saw earlier, it’s a goroutine.
5. At this point, there are two goroutines. One is the main goroutine, and the other is what we have declared.
6. Now, we are putting 10 as data in the channel, and at this point, our main goroutine is blocked and waiting for some other goroutine to read the data. The reader, in this case, is the printChannelData goroutine, which was previously blocked because there was no data in the channel. Now that we’ve pushed the data onto the channel, the Go scheduler (more on this later in the blog) now schedules printChannelData goroutine, and it will read and print the value from the channel.
7. After that, the main goroutine again activates and prints “main ended…” and the program stops.
So, what’s happening here? Basically, blocking and unblocking operations are done over goroutines by the Go scheduler. Unless there’s data in a channel you can’t read from it, which is why our printChannelData goroutine was blocked in the first place, the written data has to be read first to resume further operations. This happened in case of our main goroutine.
With this, let’s see how channels operate internally.
Internals of channels:
Until now, we have seen how to define a goroutine, how to declare a channel, and how to read and write data through a channel with a very simple example. Now, let’s look at how Go handles this blocking and unblocking nature internally. But before that, let’s quickly see the types of channels.
Types of channels:
There are two basic types of channels: buffered channels and unbuffered channels. The above example illustrates the behaviour of unbuffered channels. Let’s quickly see the definition of these:
Unbuffered channel: This is what we have seen above. A channel that can hold a single piece of data, which has to be consumed before pushing other data. That’s why our main goroutine got blocked when we added data into the channel.
Buffered channel: In a buffered channel, we specify the data capacity of a channel. The syntax is very simple. c := make(chan int,10) the second argument in the make function is the capacity of a channel. So, we can put up to ten elements in a channel. When the capacity is full, then that channel would get blocked so that the receiver goroutine can start consuming it.
Properties of a channel:
A channel does lot of things internally, and it holds some of the properties below:
Channels are goroutine-safe.
Channels can store and pass values between goroutines.
Channels provide FIFO semantics.
Channels cause goroutines to block and unblock, which we just learned about.
As we see the internals of a channel, you’ll learn about the first three properties.
Channel Structure:
As we learned in the definition, a channel is data structure. Now, looking at the properties above, we want a mechanism that handles goroutines in a synchronized manner and with a FIFO semantics. This can be solved using a queue with a lock. So, the channel internally behaves in that fashion. It has a circular queue, a lock, and some other fields.
When we do this c := make(chan int,10) Go creates a channel using hchan struct, which has the following fields:
typehchan struct { qcount uint // total data in the queue dataqsiz uint // size of the circular queue buf unsafe.Pointer // points to an array of dataqsiz elements elemsize uint16 closed uint32 elemtype *_type // element type sendx uint // send index recvx uint // receive index recvq waitq // list of recv waiters sendq waitq // list of send waiters// lock protects all fields in hchan, as well as several// fields in sudogs blocked on this channel.//// Do not change another G's status while holding this lock// (in particular, do not ready a G), as this can deadlock// with stack shrinking. lock mutex}
This is what a channel is internally. Let’s see one-by-one what these fields are.
qcount holds the count of items/data in the queue.
dataqsize is the size of a circular queue. This is used in case of buffered channels and is the second parameter used in the make function.
elemsize is the size of a channel with respect to a single element.
buf is the actual circular queue where the data is stored when we use buffered channels.
closed indicates whether the channel is closed. The syntax to close the channel is close(<channel_name>). The default value of this field is 0, which is set when the channel gets created, and it’s set to 1 when the channel is closed.
sendx and recvx indicates the current index of a buffer or circular queue. As we add the data into the buffered channel, sendx increases, and as we start receiving, recvx increases.
recvq and sendq are the waiting queue for the blocked goroutines that are trying to either read data from or write data to the channel.
lock is basically a mutex to lock the channel for each read or write operation as we don’t want goroutines to go into deadlock state.
These are the important fields of a hchan struct, which comes into the picture when we create a channel. This hchan struct basically resides on a heap and the make function gives us a pointer to that location. There’s another struct known as sudog, which also comes into the picture, but we’ll learn more about that later. Now, let’s see what happens when we write and read the data.
Read and write operations on a channel:
We are considering buffered channels in this. When one goroutine, let’s say G1, wants to write the data onto a channel, it does following:
Acquire the lock: As we saw before, if we want to modify the channel, or hchan struct, we must acquire a lock. So, G1 in this case, will acquire a lock before writing the data.
Perform enqueue operation: We now know that buf is actually a circular queue that holds the data. But before enqueuing the data, goroutine does a memory copy operation on the data and puts the copy into the buffer slot. We will see an example of this.
Release the lock: After performing an enqueue operation, it just releases the lock and goes on performing further executions.
When goroutine, let’s say G2, reads the above data, it performs the same operation, except instead of enqueue, it performs dequeue while also performing the memory copy operation. This states that in channels there’s no shared memory, so the goroutines only share the hchan struct, which is protected by mutex. Others are just copies of memory.
This satisfies the famous Golang quote: “Do not communicate by sharing memory instead share memory by communicating.”
Now, let’s look at a small example of this memory copy operation.
func printData(c chan *int) { time.Sleep(time.Second *3)data :=<-c fmt.Println("Data in channel is: ", *data)}func main() { fmt.Println("Main started...")var a =10b :=&a//create channelc :=make(chan *int) go printData(c) fmt.Println("Value of b before putting into channel", *b) c <- b a =20 fmt.Println("Updated value of a:", a) fmt.Println("Updated value of b:", *b) time.Sleep(time.Second *2) fmt.Println("Main ended...")}
And the output of this is:
Main started...Value of b before putting into channel 10Updated value ofa: 20Updated value ofb: 20Data in channel is: 10Main ended...
So, as you can see, we have added the value of variable a into the channel, and we modify that value before the channel can access it. However, the value in the channel stays the same, i.e., 10. Because here, the main goroutine has performed a memory copy operation before putting the value onto the channel. So, even if you change the value later, the value in the channel does not change.
Write in case of buffer overflow:
We’ve seen that the Go routine can add data up to the buffer capacity, but what happens when the buffer capacity is reached? When the buffer has no more space and a goroutine, let’s say G1, wants to write the data, the go scheduler blocks/pauses G1, which will wait until a receive happens from another goroutine, say G2. Now, since we are talking about buffer channels, when G2 consumes all the data, the Go scheduler makes G1 active again and G2 pauses. Remember this scenario, as we’ll use G1 and G2 frequently here onwards.
We know that goroutine works in a pause and resume fashion, but who controls it? As you might have guessed, the Go scheduler does the magic here. There are few things that the Go scheduler does and those are very important considering the goroutines and channels.
Go Runtime Scheduler
You may already know this, but goroutines are user-space threads. Now, the OS can schedule and manage threads, but it’s overhead to the OS, considering the properties that threads carry.
That’s why the Go scheduler handles the goroutines, and it basically multiplexes the goroutines on the OS threads. Let’s see how.
There are scheduling models, like 1:1, N:1, etc., but the Go scheduler uses the M:N scheduling model.
Basically, this means that there are a number of goroutines and OS threads, and the scheduler basically schedules the M goroutines on N OS threads. For example:
OS Thread 1:
OS Thread 2:
As you can see, there are two OS threads, and the scheduler is running six goroutines by swapping them as needed. The Go scheduler has three structures as below:
M: M represents the OS thread, which is entirely managed by the OS, and it’s similar to POSIX thread. M stands for machine.
G: G represents the goroutine. Now, a goroutine is a resizable stack that also includes information about scheduling, any channel it’s blocked on, etc.
P: P is a context for scheduling. This is like a single thread that runs the Go code to multiplex M goroutines to N OS threads. This is important part, and that’s why P stands for processor.
Diagrammatically, we can represent the scheduler as:
The P processor basically holds the queue of runnable goroutines—or simply run queues.
So, anytime the goroutine (G) wants to run it on a OS thread (M), that OS thread first gets hold of P i.e., the context. Now, this behaviour occurs when a goroutine needs to be paused and some other goroutines must run. One such case is a buffered channel. When the buffer is full, we pause the sender goroutine and activate the receiver goroutine.
Imagine the above scenario: G1 is a sender that tries to send a full buffered channel, and G2 is a receiver goroutine. Now, when G1 wants to send a full channel, it calls into the runtime Go scheduler and signals it as gopark. So, now scheduler, or M, changes the state of G1 from running to waiting, and it will schedule another goroutine from the run queue, say G2.
This transition diagram might help you better understand:
As you can see, after the gopark call, G1 is in a waiting state and G2 is running. We haven’t paused the OS thread (M); instead, we’ve blocked the goroutine and scheduled another one. So, we are using maximum throughput of an OS thread. The context switching of goroutine is handled by the scheduler (P), and because of this, it adds complexity to the scheduler.
This is great. But how do we resume G1 now because it still wants to add the data/task on a channel, right? So, before G1 sends the gopark signal, it actually sets a state of itself on a hchan struct, i.e., our channel in the sendq field. Remember the sendq and recvq fields? They’re waiting senders and receivers.
Now, G1 stores the state of itself as a sudog struct. A sudog is simply a goroutine that is waiting on an element. The sudog struct has these elements:
typesudog struct{ g *g isSelect bool next *sudog prev *sudog elem unsafe.Pointer //data element ...}
g is a waiting goroutine, next and prev are the pointers to sudog/goroutine respectively if there’s any next or previous goroutine present, and elem is the actual element it’s waiting on.
So, considering our example, G1 is basically waiting to write the data so it will create a state of itself, which we’ll call sudog as below:
Cool. Now we know, before going into the waiting state, what operations G1 performs. Currently, G2 is in a running state, and it will start consuming the channel data.
As soon as it receives the first data/task, it will check the waiting goroutine in the sendq attribute of an hchan struct, and it will find that G1 is waiting to push data or a task. Now, here is the interesting thing: G2 will copy that data/task to the buffer, and it will call the scheduler, and the scheduler will put G1 from the waiting state to runnable, and it will add G1 to the run queue and return to G2. This call from G2 is known as goready, and it will happen for G1. Impressive, right? Golang behaves like this because when G1 runs, it doesn’t want to hold onto a lock and push the data/task. That extra overhead is handled by G2. That’s why the sudog has the data/task and the details for the waiting goroutine. So, the state of G1 is like this:
As you can see, G1 is placed on a run queue. Now we know what’s done by the goroutine and the go scheduler in case of buffered channels. In this example, the sender gorountine came first, but what if the receiver goroutine comes first? What if there’s no data in the channel and the receiver goroutine is executed first? The receiver goroutine (G2) will create a sudog in recvq on the hchan struct. Things are a little twisted when G1 goroutine activates. It will now see whether there are any goroutines waiting in the recvq, and if there is, it will copy the task to the waiting goroutine’s (G2) memory location, i.e., the elem attribute of the sudog.
This is incredible! Instead of writing to the buffer, it will write the task/data to the waiting goroutine’s space simply to avoid G2’s overhead when it activates. We know that each goroutine has its own resizable stack, and they never use each other’s space except in case of channels. Until now, we have seen how the send and receive happens in a buffered channel.
This may have been confusing, so let me give you the summary of the send operation.
Summary of a send operation for buffered channels:
Acquire lock on the entire channel or the hchan struct.
Check if there’s any sudog or a waiting goroutine in the recvq. If so, then put the element directly into its stack. We saw this just now with G1 writing to G2’s stack.
If recvq is empty, then check whether the buffer has space. If yes, then do a memory copy of the data.
If the buffer is full, then create a sudog under sendq of the hchan struct, which will have details, like a currently executing goroutine and the data to put on the channel.
We have seen all the above steps in detail, but concentrate on the last point.
It’s kind of similar to an unbuffered channel. We know that for unbuffered channels, every read must have a write operation first and vice versa.
So, keep in mind that an unbuffered channel always works like a direct send. So, a summary of a read and write operation in unbuffered channel could be:
Sender first: At this point, there’s no receiver, so the sender will create a sudog of itself and the receiver will receive the value from the sudog.
Receiver first: The receiver will create a sudog in recvq, and the sender will directly put the data in the receiver’s stack.
With this, we have covered the basics of channels. We’ve learned how read and write operates in a buffered and unbuffered channel, and we talked about the Go runtime scheduler.
Conclusion:
Channels is a very interesting Golang topic. They seem to be difficult to understand, but when you learn the mechanism, they’re very powerful and help you to achieve concurrency in applications. Hopefully, this blog helps your understanding of the fundamental concepts and the operations of channels.