Longtime readers of CODE Magazine will recognize that I'm very much a lover of programming languages; in addition to being an F# MVP (I wrote the first F# language article for CODE a few years ago), and writing code in Java, C#, JavaScript, and iOS on a regular basis, I love to dally with other languages as I find them. I've written Ruby (both for its native execution environment as well as for the JVM and CLR variants), ML (for both native and JVM variants), OCaml, Nemerle, Boo, Haskell, Frege, Clojure, Scheme, Lisp, D, and I've even sometimes written my own.
Yes, I admit it, I have a problem. But hey, it's better than being addicted to Flintstones reruns, and besides, I can quit anytime I want. Really.
Thus, it was with no hesitation that when Rod Paddock was looking for someone to tackle the Go language for CODE Magazine, I was quick to throw my virtual hand into the air. (Actually, I threw my real hand in the air, and then realized that he couldn't see it over Twitter. The others on the airplane around me, however, were a little confused.) Bear in mind, however, that it's impossible to explore every nook and cranny of a language in a single magazine article; there are interesting bits of syntax that I'm deliberately passing over, so consider this a teaser designed to make you want to explore further.
Shall we?
History and Motivation
In March of 2012, when Google first released version 1.0 of Go, it was met with a curious mix of skepticism and excitement. Many observers were excited about it, because the principals involved in its inception, Brian Kernighan and Rob Pike, have been longtime contributors to some very positive and popular tools and projects. (Kernighan, in particular, is the first half of Kernighan and Ritchie, the authors of the seminal book on C, also known as the K&R book.) Others, however, were skeptical, owing to sentiments like “the world doesn't really need another systems language,” and mutterings about Google's intentions around it. (For the record, I was in the latter category for several years - I didn't think it had much of a chance.)
By the time the most recent release, Go 1.6, shipped in February of 2016, many of the naysayers had forgotten about Go, changed their minds, or simply moved on. Quietly but steadily, Go has gained ground, slowly becoming a viable candidate for building systems. It's been used in several interesting projects, including Hugo (https://gohugo.com - Editor's note: link no longer works) and the smaller project that's the engine I use for my new blog, for example. It may not replace C or C++ as the main player for building native executables any time soon, but it's had seven releases in a fairly stable cadence across the last four years, and shows no signs of slowing down.
Thus, it's time to take a serious look at the language - even if you never end up using it on a project, it has some intriguing ideas around the design of code and programming languages, and chances are that there's something in those ideas that can be useful on your next .NET project.
Enough fluff; let's Go (pun intended) and write some code. It's going to be a whirlwind, so strap in and hang on. Ready?
Installation
Getting Go isn't difficult and it's free. The Go language website (https://golang.org) has an easy top-level link that takes visitors to an install page (https://golang.org/doc/install) containing installation instructions; on a Mac, it's as easy as “brew install go” (assuming you have Homebrew installed; if you don't use Homebrew, seriously, what's wrong with you?), and on Windows, there's an MSI for easy installation.
Once installed, however, Go requires a little bit more configuration (regardless of platform) before you can get going. Contrary to the attitude espoused by other languages (C, C++, C#, and so on), Go puts requirements down around the physical arrangement of files (source and otherwise). Specifically, Go requires that Go code live inside of a workspace
, which is a physical directory that's the directory tree in which you're developing your program. This workspace must be listed in the GOPATH
environment variable before most of the tools will be able to operate correctly. Thus, for example, on my Mac, I can point GOPATH to my local directory by writing:
export GOPATH=`pwd`
This is a bit of Bash shell magic that takes whatever the contents of pwd
(print working directory) are and sets that to be the contents of the GOPATH
environment variable. (It's /Users/tedneward/Projects/Publications/Articles/CODRGo
as I write this, just in case you were wondering.)
This environment variable is highly reminiscent of the Java CLASSPATH
environment variable, as they are both used for exactly the same thing: to help the Go compiler resolve import statements. In other words, when you compile Go code, Go looks at the contents of GOPATH
, and starts looking for dependent libraries underneath that directory. Convention holds that GOPATH
contains only one directory statement, but in fact (as the Go command-line documentation will tell you if you type go help gopath once Go is installed on your computer), it can contain as many different directories as desired, so long as they are appropriately separated (using a semi-colon (;) on Windows and a colon(:) on Mac or Linux), exactly in the same manner that the PATH environment variable operates.
Unlike CLASSPATH
, however, GOPATH
directories are a mix of source, binaries, and packages (which I'll talk about in a second). Each GOPATH
directory (a Go workspace) will have a src
subdirectory for source code, a bin
directory for holding compiled commands, and a pkg
directory for holding installed package objects. This will all make more sense over time, but for now, just keep this in mind as you create new Go programs.
As a matter of fact, it's about time to honor the Tiki Gods of Computer Science (particularly because one of them was involved in creating the language in the first place!), so let's write Hello World.
Hello, Go
Assuming that Go is correctly installed (type go version at the command-line after installation; Go installs itself into your PATH, so if it doesn't work, try opening a new command-line window or reboot the computer after installation), create a workspace by creating a directory of your choice, and set the GOPATH
to point to it. Within that workspace, create an src
directory, and within that, let's create a new file, hello.go
. In it, put:
package main
import "fmt"
func main() {
fmt.Printf("Hello, CODE\n")
}
Now compile the file using go build hello.go. The Go compiler thinks for a second, returns, and a new executable (hello
or hello.exe
) appears in the current directory. Go ahead, run it - it'll say hello, and you can chalk that off the checklist. Alternatively, if you prefer, Go has a built-in compile-and-run command, called go run, which can do the same thing - go run hello.go prints your greeting to the console.
As it turns out, the go
command has quite a list of high-level tooling built into it beyond mere compilation; you'll see some of it as you continue, but running go
by itself (no arguments) prints the high-level list, and go help whatever lists the help text for the whatever
command of go
. (No, there's not a “whatever” command, that's my placeholder for something else, like run
or build
. Whatever.)
By the way, don't be too alarmed at the size of the executable (22MB on my Mac); contrary to the CLR (and the JVM, for that matter), a Go executable is a statically linked executable, so it can be copied entirely standalone with zero supporting infrastructure required. This is important to note - it's common for .NET developers to assume that the CLR is installed on any computer on which an application wants to run, but in some environments that's easier said than done. (See Joel Spolsky's essay “Please sir, may I have a linker?” for a deeper dive on this subject. http://www.joelonsoftware.com/articles/PleaseLinker.html)
In addition, when you start talking about containerization (Docker and the like), which suggests that each application runs in its own virtual machine environment, that hundred-plus-MB install of the CLR becomes a heavy dead weight. However, regardless of whether you agree with this analysis or not, it highlights an important difference between the Go and .NET environments: Go programs are system programs, and want to run without any explicit external dependencies; .NET programs are application programs. It's a subtle difference, particularly as the Venn diagram of what each can do is quite overlapping, but it does explain some of how each community views itself.
Packages
There are some interesting things of note in the code above, and I'll get to a lot of it, but one of the first things to realize is that all Go code must live inside of a package. The hello
program above is declared to be in the main
package, and this is not by accident - Go requires that the main()
entry point must be declared in the main
package, so that Go can identify which function represents the first line of code to execute when launched. (You probably also noticed that main()
is not inside of any class or object - I'll get to that in a second, I promise.)
Because Go's package management is a key feature of Go, let's take a moment and explore that.
Let's create your own package, and put the greeting message into a library for later reuse. (You know, it's always nice to say hello, and we want to be all DRY (Don't Repeat Yourself) about the whole thing, in case we ever want to say “Hello” from another program, right?)
To start, you need to move the location of the source file; Go requires that the paths to the source files match the package names in a very particular way. If you want to create a package called (for lack of anything original) “hello,” the source needs to be in a hello
subdirectory. But not just any hello
subdirectory - in order to help ensure that your hello
package doesn't collide with anybody else's, you need to put it in a unique subdirectory path, because the directory names combines to form the import path of the package. Go suggests that if you're going to store the source in a source repository somewhere, it makes sense to put the online repository path as part of the directory path.
Because I want to store the source to hello
in my GitHub account, I create a subdirectory called github.com
, one inside github.com
called tedneward
(which is my GitHub username), and then inside that, hello
. This directory (src/github.com/tedneward/hello
) is also often the Git root of the repository, because a non-trivial program is made up of several packages, and I'll want each package versioned and maintained separately. (There are other reasons to do that, too, which you'll see in a second.) I'll take this moment to rename the package to gohello
, by the way, so that it'll match the name of the repository I'm about to create up on GitHub; having the package name and the repository name match is an important part of this.
Once the src/github.com/tedneward/gohello
directory is created, move the unchanged hello.go
source file into it (so the complete file path for that source file will be <workspace>/src/github.com/tedneward/gohello/hello.go
), and then from anywhere inside the workspace, type go install github.com/tedneward/gohello. It compiles the file and installs it into another directory at the root of the workspace, the bin
subdirectory I mentioned earlier. This is partly why Go calls these working directories workspaces, rather than a term like Visual Studio's solution - the Go workspace will have source code, binaries, and (later) referenced packages.
By the way, for those of you playing the home game, you can fork my repository onto your own on GitHub, and use that instead. You'll just need to make sure that you use your username instead of mine for the rest of the article.
Thus far, gohello
is still an executable and not a library. To convert it, you need to declare it to be in its own package (which will be gohello
, to match that of the GitHub repo in which it is stored):
package gohello
import "fmt"
func SayHello() {
fmt.Printf("Hello, CODE\n")
}
Then, because now you need a main
again, create a directory called hello
inside of tedneward
, and into it, put a slightly modified main executable Go
file:
package main
import "github.com/tedneward/gohello"
func main() {
gohello.SayHello()
}
Notice that now, instead of importing the fmt
package (from the standard Go library), you import github.com/tedneward/gohello, which is that package's full name. Anything outside of the Go standard library wants to be qualified like this, to avoid conflict with other packages outside of the Go ecosystem (like mine). Additionally, when using the gohello
package, Go automatically uses the short name of the package as its code-qualifier when using the package. And because the SayHello
function is defined in that package, accessing it uses the short name plus the function name, as in gohello.SayHello
.
Once the new main
package is written (and I called it hello.go
again - the file's name doesn't matter to Go, only the directory paths to get to it), from the hello
subdirectory, execute go run hello.go, and you see the familiar greeting.
The file's name doesn't matter to Go, only the directory paths to get to it.
What if you want to use both the aforementioned Printf
and SayHello
? Easy - you import them both:
package main
import (
"fmt"
"github.com/tedneward/gohello"
)
func main() {
fmt.Printf("Hey, world, important message:")
gohello.SayHello()
}
Coolness. But if you've been watching the source files go by and wondering “Where are all the objects?” you're in a for a bit of a shock. Go is not an object-oriented language - at least, not by .NET's standards.
Go Basics
Let's look at some of the things that Go is: It's an imperative, strongly typed, type-inferenced language, based (loosely) on the other C-family languages with which most of us are familiar. For the most part, it uses the same type system as what a C-based language would use, meaning that there are strings, integer and floating-point numeric types, and so on. There are definitely differences, but for the most part, that's enough to get you going. What's more, most variables will be type-inferenced, meaning that you can declare them with the keyword var
, and not worry about what the syntax of the type will be, like this:
var message = "Hello, CODE world"
Go, like most imperative languages, supports the notion of a named block of code, keyed using func
in Go and corresponding to function
or procedure
in other languages. It can take parameters and return values, just like C procedures, but there are a couple of interesting differences. First, the name matters - if the method name begins with an upper-case letter, that function is exported, meaning that it's visible outside of the package; lowercase-starting names are not visible outside of the package. (Try it - rename SayHello
to sayHello
, and notice how main()
can no longer access it.) This is how Go provides encapsulation without explicit public or private keywords.
This naming convention applies equally to both functions and variables, so you can define a Message
variable that will be exported outside the package, along with a package-local original
that allows you to go back to the original message at any time:
package gohello
import "fmt"
var original = "Hello, CODE world"
var Message = original
func SayHello() {
fmt.Printf("%s\n", Message)
}
func Reset() {
Message = original
}
This Message is now accessible outside of the package, such as from main()
:
func main() {
fmt.Printf("Hey, world, an important message:")
gohello.SayHello()
gohello.Message = "Fred Flintstone"
gohello.SayHello()
}
If you're wondering what the %s
is in the Printf()
call (or the \n
, for that matter), you can find the full documentation for all Go packages online at https://golang.org/pkg/, and the fmt
package specifically at https://golang.org/pkg/fmt. On that page, you're informed that %s
is the placeholder syntax for a string value to be supplied in a follow-up argument, just as Console.WriteLine
works. However, unlike WriteLine()
, Printf()
it's positionally based, so the first %s
is matched up against the first argument in the argument list, rather than using numeric **{0}-
**style indicators. (If you can't remember which format code goes with which types, fall back to the generic %v
; that says to use the Go-defined value
representation of whatever is passed. Most of the time, it'll do the right thing.)
If you can't remember which format code goes with which types, fall back to the generic
%v
.
If this is starting to feel a lot like C, did I mention that one of Go's core founders was the Kernighan from the original C programming language book?
As mentioned, functions can take parameters, but the type descriptor is written after the parameter name, not before, and the return type is listed after the parameter list, like so:
func PigLatinizer(msg string) string {
return msg + "way"
}
What if the PigLatinizer
function has a problem PigLatinizing the incoming message? Normally, this is where a language like C# either throws an exception or returns null
or some other placeholder value to indicate an error; Go uses multiple return values instead, like so:
func PigLatinizer(msg string) (bool, string) {
return true, msg + "way"
}
Dealing with multiple return values is a touch trickier:
fmt.Println(gohello.PigLatinizer("Hello"))
Writing that prints out true Helloway, because the returned value is actually a pair of values (a tuple), and Go prints out that value
as both of the underlying values (true
and the “way”-suffixed string). To extract the success or failure of the operation separately from the actual string, you need to use a multiple-assignment statement, like so:
var success, piglatin =
gohello.PigLatinizer("Hello")
if success {
fmt.Println(piglatin)
}
Other languages sometimes refer to this kind of approach as “destructuring assignment”, because you're breaking the structure of the tuple into multiple parts (the local variables success
and piglatin
).
When using multiple return values, by the way, it can sometimes be confusing to have to return both simultaneously, so Go allows you to write named return values, like so:
func PigLatinizer(msg string) (success bool, returnVal string) {
success = true
returnVal = msg + "way"
return
}
The usage on the caller side remains unchanged.
By the way, you've just seen how an if
works - notice how the parentheses from C# are missing? Go doesn't need the parentheses to surround the true
/false
expression. (As a matter of fact, most languages require them out of habit, not need.)
On the surface, Go has similar control constructs to what you see in C-family languages, such as for
loops and switch
/case
evaluation, but just a tiny bit of digging reveals that the people working on Go have had a few new ideas since 1970. If statements, for example, can have an execution statement right before the condition, for example, like this:
if x := f(); x < y {
return x
} else if x > z {
return z
} else {
return y
}
Similarly, for
loops in Go are a merge of the traditional for
loop and the classic while
or do
/while
loop, in that the for
loop requires some Boolean expression, which is evaluated continuously (before each iteration of the loop) until the condition yields false
.
for a < b {
a *= 2
}
When combined with the preceding executable statement ability, you have the ability to mimic a classic C-style for
, like this:
for i := 0; i < 10; i++ {
f(i)
}
Most often, however, a for
iterates across a range of values (such as that in a collection), similar to the foreach
in C#, and thanks to destructuring assignment, the for
returns both an index and the current element if you wish, like this:
var a [10]string
for i, s := range a {
// type of i is int
// type of s is string
// s == a[i]
g(i, s)
}
I'll talk about what that a
is and what a range
is in a second. You can probably guess, and you're about half right.
Go also has switch
statements, which have the ability to do the prefixing statement that if
has. Switch
can also switch on the type of a variable (rather than its value), if desired.
Go also isn't only resting on the traditional C-family constructs.
But Go also isn't only resting on the traditional C-family constructs; in addition to the above, Go has introduced a new control construct, defer
, which takes an expression (or an anonymous function block) that will be executed when this block of code terminates. In other words, Go's defer
provides destructor-like syntax but without having to define an explicit IDispose interface implementation or complicated C++-style destructor rules. You could write this:
func DemoDefer() {
fmt.Println("Entering DemoDefer")
defer fmt.Println("In defer the first time")
defer func() {
fmt.Println("In defer the second time")
}()
fmt.Println("Exiting DemoDefer")
}
When it's executed, Go prints out:
Entering DemoDefer
Exiting DemoDefer
In defer the second time
In defer the first time
Notice that the deferred expressions are executed in the reverse order of their definition - it's exactly stack-like behavior (and it mimics the C++ object destructor rules, not that anybody's intrigued by that besides me).
Let's keep moving.
Complex Go Types
I said earlier that Go lacks any sort of object facilities. This is true, but Go does allow you to define three different kinds of more complex types: arrays (which look an awful lot like C-style arrays), structures (which look an awful lot like C-style arrays), and maps (which look an awful lot like C# System.Collection.Generic.Dictionary<>
types).
Arrays are the easiest one to define: using the []
syntax, you can define a collection of singly typed elements into continuous storage, like so:
as := make([]int, 10, 100)
This creates an array of integers, currently ten items in length (but everything will be initialized to 0), and with a top capacity of 100 elements in size. Notice the difference - only ten items are currently allocated, but the Go runtime makes room for up to 100. It's an array, like C#-style arrays, but it can be appended and grown, like a List<T>
, up to a finite amount. The make
here is a built-in function, similar in concept to C#'s (or Java's or C++'s) new
. Go also uses new
, but under different circumstances, and like make
, new
is just a built-in function with no other special syntax or rules. Several other built-in functions can manipulate an array (append
, copy
, and delete
, for example), and I'll leave it to the Go documentation to fill in those details.
The related type slices
are essentially sub-ranges of an underlying array. For example, you can take a slice
of the above array by taking the values from the second position through the fifth by asking for them using the []
operator. (Technically, when you index into the array for a single element, you're getting a slice
of length 1.)
Maps
are, like arrays, a collection of elements, but these are associative arrays, meaning that you can define the keys used to identify the elements stored within. Creating a map of strings to integers, for example, looks almost identical to the array syntax, except that now the type descriptor is slightly different:
ages := make(map[string]int, 100)
ages["Ted"] = 45
ages["Charlotte"] = 29
ages["Michael"] = 23
ages["Matthew"] = 16
for i, a := range ages {
fmt.Printf("%v is %v years old\n", i, a)
}
This creates a map of 100 string/integer pairs (the capacity is assumed to be the same as the length if no other value is given), all of which will be initially empty. You then use the string as the key, assign some ages to the map
, and finally iterate through the map
(using the range
syntax). The variable i
receives the index (the key), and the variable a
receives the value at that index (the age).
Structures
are, as their name implies, the means by which Go defines custom data types; a structure
can consist of one or more other types, that is, fields, that either have names or use their type definitions as names (and are thus known as anonymous fields). For example, to define a Person
type in Go, it would look like:
package goperson
type Person struct {
FirstName string
LastName string
Age int
}
Note that you've defined this in a new library, goperson
; this is so you can demonstrate how Go can support remote libraries. Back in the main
package, you need to add the code that uses the Person
type, but more importantly, you need to import it as usual:
package main
import (
"fmt"
"github.com/tedneward/gohello"
"github.com/tedneward/goperson"
)
func main() {
// . . .
ted := goperson.Person{"Ted", "Neward", 45}
fmt.Printf("ted = %v\n", ted)
fmt.Println("ted.FirstName =", ted.FirstName)
}
There are a couple of things to note here. First, if you try to go run this code, goperson
won't be found anywhere. There's one more step you need to do before it'll all work. At the command-line, from anywhere inside the workspace, type go get github.com/tedneward/goperson. This fetches the package from the source control repository specified, and installs it locally within the workspace. Go won't automatically update the local source based on changes (it doesn't check across the network on each build, which is fortunate), so if the library changes, make sure to capture the updates by using go get again with the -u (for update
) flag turned on: go get -u github.com/tedneward/goperson.
Second, notice how I'm using the ted
variable without having declared it before now. That's deliberate. As a shortcut, Go allows the definition and initialization of a variable simultaneously using the := syntax, as above. (As a matter of fact, I've used it a couple of times before now - go back and check if you didn't see it earlier.)
In order to be accessible outside the package, the Person
type must be upper-cased, as shown, and the fields must be upper-cased as well. Again, this is how Go defines those fields that can be seen outside of the package and those that can't. To the .NET developer, this naming convention is already familiar.
Next, to define an instance of the type Person
, you can construct an instance using the {}
syntax, which looks (and acts) much like a constructor that's automatically defined for each of the fields inside the struct. Technically, ted
is a pointer to the Person
instance, and although Go does support C-like pointer syntax (as in *var ted Person), there are several cases where Go understands what's intended even when the pointer operators (*
and &
) are omitted, such as the above.
Note that if a Go developer absolutely can't live without something that looks like an object, you can always define fields of a struct to be of function types (func(int) int for a function that takes an int and returns an int), created using a constructor function such as NewPerson
defined in the goperson
package. (In fact, idiomatic Go prefers such a function over using the initializer syntax that I used above.) Packages go a long ways toward masquerading as objects, and clever use of functions and structs can remove all but the most strenuous objections (pun intended).
In a fit of sympathy, Go also allows method sets to be defined for a specified type by defining a function that takes an explicitly declared this pointer, like so:
func (p Person) Fullname() string {
return p.FirstName + " " + p.LastName
}
Go offers interfaces as well, which provide declarations of expected behavior (in terms of these method sets) defined for a specified type. This delivers the same capacity of any object-oriented language, with the exception of implementation inheritance. There's much more to go into (pun intended), but Go defines one additional construct that makes Go really interesting. That's concurrent routines.
Concurrently Routine Go
One of the keys that makes Go interesting is its concept of lightweight concurrent processing elements, called a goroutine. This's an asynchronous execution object, similar in some ways to a thread, but lighter weight than threads (because several goroutines can execute on a single operating system thread), making them more akin to an Erlang actor than a thread. Go uses the go
keyword to indicate the creation of a goroutine, and the expression(s) referenced in the go
command are immediately scheduled for execution by a goroutine.
Goroutines aren't threads, despite the surface appearance, and any synchronization between goroutines requires the use of the final Go complex type, a channel. The channel acts as a bounded buffer between goroutines, unidirectional in nature and allowing any sort of type to be placed within it. When pulling from the channel, the goroutine is blocked until there's a value available, and when pushing into the channel, the goroutine is blocked unless the channel has room for the value. By default, channels can hold one strongly typed value, unless declared to hold more.
Adapting from a popular Java producer-consumer concurrency example, you create two functions, one that produces a string by pushing it into a shared buffer (a chan string in Go terminology):
func Produce(buffer chan string) {
lyrics := [4]string{
"Mares eat oats",
"Does eat oats",
"Little lambs eat ivy",
"Wouldn't you like to eat ivy too"}
for _, s := range lyrics {
buffer <- s
}
close(buffer)
}
Notice that the Produce
function calls the built-in close
function on the channel when the array has been fully pushed into the channel. This is how the consumer knows that there's nothing left to consume.
Next, you define a Consume
function that reads from that channel until there's nothing left to read:
func Consume(buffer chan string, quit chan int){
for msg := range buffer {
fmt.Println("We received:", msg)
}
quit <- 0
}
The for
loop here automatically pulls from the channel (because you use it as the source of the for
loop's data); this is a pretty common thing to do. Were this not the case, Consume
could pull a single value out of the channel by calling <- buffer
, storing the value returned into a local variable. (Yes, that's the left-angle-bracket and the hyphen; think of them as a new operator, like the assignment operator, but consuming an element out of the channel as it's doing the assignment.)
Once that channel is empty, Consume
sends a message on a second channel (quit
) to tell the main
goroutine that it's time to quit; otherwise, without this quit
channel, main
finishes and terminates before the producer and consumer get their chances to do their respective things. The main
goroutine needs to block on quit
until it receives the signal, giving me a chance to show you some more syntax:
buffer := make(chan string, 4)
quit := make(chan int)
go Produce(buffer)
go Consume(buffer, quit)
select {
case <-quit: fmt.Println("quit")
}
Notice that buffer
and quit
are both constructed using the built-in make
function again. The buffer channel is declared to be four elements in size, allowing the Produce
function to run at full-tilt, but the code would work just as well if the buffer were only one element in size - it ping-pongs back and forth between Produce()
and Consume()
until all the messages have been consumed. (Here's a simple exercise for those following the code on their laptops: put some Println()
statements into Produce()
and Consume()
and prove this.) Then main()
uses the go
keyword to launch Produce()
and Consume()
on goroutines, and uses the select
keyword, which is much like a switch, to block until the quit
channel produces a value (that you ignore, because merely being signaled is enough).
The code works just as well if the buffer is only one element in size - it ping-pongs back and forth between
Produce()
andConsume()
until all the messages are consumed.
There's much, much more that can be explored here, particularly since Go's simplicity around concurrency enables a number of very elegant solutions to some very complicated concurrent execution, pushing it in terms of time (and cognitive capacity).
Summary
If you made it this far, congratulations, particularly if you made it all the way through in one reading. This wasn't an easy ride; take a deep breath, let it out slowly, and let your mind clear for a moment.
The key takeaways here about Go are fairly straightforward. First, Go is a system-level language; it's not intended to execute on top of a virtual machine. As a matter of fact, a number of projects are exploring using Go as the source language for a virtual machine for other languages. Most of the time, Go won't be the language with which developers build the front-end, but rather the infrastructure and/or the back-end services. (As a matter of fact, the Go standard library has extensive support for various network protocols, encryption, and network security, and wire representations like JSON.)
As an example of what can be done very elegantly in Go, consider the NATS message server, available at http://nats.io/ - it's a high-performance messaging service, written in Go, that defines a fairly simple messaging protocol based roughly on HTTP (meaning it defines its own protocol using some of the same concepts that defined HTTP). It vastly outstrips other messaging plumbing, claiming throughput that is twice as large as Redis or ruby-nats, and close to ten times greater than that of ActiveMQ, or RabbitMQ.
Second, Go is not an object-oriented language, if you think of such a language as having implementation inheritance. This is familiar ground for those who lived through the COM Object Wars fifteen years ago. Fortunately, the industry as a whole seems to be past its obsessive fascination with what makes a language object-oriented (and therefore good), so perhaps this isn't as big an issue as it might once have been.
Third, Go's concurrency constructs wouldn't be hard to replicate in .NET, assuming that Microsoft provided a lighter-weight construct than a thread that was creatable from C# code. (A long time ago, Microsoft created fibers, which were essentially coroutines like Go goroutines, but they've since fallen out of favor among the Microsoft cognoscenti, it seems.) Nevertheless, Microsoft has been experimenting with concurrency constructs like this for some time, particularly the channels concept, which was the subject of a Microsoft Research language called Axum a number of years ago. (I wrote an article on Axum back in 2010, available at https://msdn.microsoft.com/ko-kr/magazine/ee412254.aspx, for those who are interested.) Axum was discontinued, but the ideas are clearly still floating around the Microsoft campus. Other, similar, concepts are being pursued in the open-source arena, so if this approach to concurrency seems appealing, by all means, fire up the search engine.
Of course, the Go ecosystem is an interesting place in its own right, and definitely worth exploring, particularly in concert with some of the containerization efforts under way. This is, in fact, one of the approaches the NATS team suggests for running their messaging server, and definitely represents a new way of thinking about deploying services.
Finally, I just can't end the article without one more pun: Good luck with your Go research, and remember, no matter what, keep Going!