In case you are a Node.js developer (like we are at RisingStack) and you are interested in learning Golang, this blogpost is made for you! Throughout this tutorial series, we'll cover the basics of getting started with the Go language, while building an app and exposing it through a REST, GraphQL and GRPC API together.

However, a disclaimer is needed before we get started: while I have written several Go programs before, I am less experienced with it than with Node.js or Python so in case you find pieces of code that don't follow best practices, please make sure to let me know!

In the first part of this golang tutorial series, we’re going to cover:

  • Golang Setup
  • net/http with Go
  • encoding/json
  • dependency management
  • build tooling

You can find the final version of the source code for this blog post on GitHub.

(Also, I started a discussion on Reddit about learning Go as a Node developer - I'd love to hear your input here)

A more detailed Table of Contents if you'd like to jump around, as this is quite a long post. :)


Things You Should Know About Golang as a Node.js Developer

At RisingStack we’ve been building webapps for our clients with Node.js for ~5 years - therefore we’ve seen the rise of Node from a relatively unknown, edgy technology to the mainstream status it enjoys Today.

If you take a look at the history of Node.js, you can see that it is now widely used in enterprise environments as well. However, we couldn’t miss the fact, that in the past years Go emerged as the interesting and hyped new back-end language.

Go is becoming the new enterprise language besides Java and C# and for a good reason.

Go is almost as simple as a scripting language like Python or JavaScript but generates a binary, so after compilation, you can run it without the need to install any additional runtime. Go also features very nice concurrency primitives and provides excellent speed if used right.

However, the number of libraries compared to other platforms is small, and they tend to be young. This could be explained by the current developer trend of dependency-hell aversion, but it can still hurt development speed. Another drawback is that because Go is statically typed and lacks generics, you cannot use basic functional programming niceties such as map, filter, reduce, unless you write them for each type or generate the code for them.

The latter will be mended in the coming v2.0, but until then we have to use what we have, which is more than enough.


Before We Start this Tutorial: Tour of Go

Go has an awesome set of resources for those who are getting started. The language can be learned just by reading the specification. However, it might be quicker and provide a better experience to complete A Tour of Go, which will interactively teach you the basics of the language. I will heavily rely on the knowledge the Tour provides, so this is high time to complete it if you haven’t yet done so. Go ahead, I’ll be waiting.

Done?

Good, now we can get started.


Getting Started With Golang

While your package manager of choice may ship Go for you, it’s highly recommended to download the binary yourself, so you can easily switch between versions if needed, but prevent the package manager from updating it for you when you don't expect it.

There's a reason we use nvm for Node after all.

So go grab the binary from the official site for your OS and CPU arch, and place it somewhere safe on your machine. I keep mine at $HOME/go/, and if you do so as well, make sure you add $HOME/go/bin to your PATH, so you can run go get, go build and their friends. Also, make sure to set the GOROOT env var to point to $HOME/go/, or wherever you extracted the tarball.

Your .bashrc should have something like these two extra lines at the moment:

export GOROOT=$HOME/go
export PATH=$PATH:$GOROOT/bin

The GOROOT env var is needed for two reasons: one is that you can download other versions of go just like you can do of Node with nvm.

$ go get golang.org/dl/go1.10.7
$ go1.10.7 download

Setting GOROOT makes sure other versions are downloaded to the right place.

The other reason can be seen if you take a look at the contents of the extracted tarball.

$GOROOT/
├── api
├── bin
│   ├── go
│   ├── godoc
│   └── gofmt
├── doc
│   ├── articles
│   ⋮
│  
├── lib
│   └── time
├── misc
│   ├── android
│   ⋮
│  
├── pkg
│   ├── include
│   ⋮
│  
├── src
│   ├── bufio
│   ├── bytes
│   ├── compress
│   ├── container
│   ├── context
│   ├── crypto
│   ├── database
│   ├── debug
│   ├── encoding
│   ├── errors
│   ├── expvar
│   ├── flag
│   ├── fmt
│   ⋮
│  
└── test
    ├── 235.go
    ⋮

The dir we're interested in is src here. We can find many of the packages you've met during the Tour of Go, such as fmt. When we import these packages go needs to find them so it can compile them with our code. GOROOT tells go where to look for the sources of the standard library.

We are not done yet, as we need to set our GOPATH as well. That's where the packages we download with go get will be downloaded and compiled. Create a directory somewhere with bin and src subdirs.

$ mkdir -p $HOME/projects/go/bin $HOME/projects/go/src

src is where the source codes will be downloaded when you go get a package, and they will be compiled and installed in bin. Thus, if you wish to use any program installed by go get, you need to add $GOPATH/bin to your path as well. So finally, your .bashrc should look something like this:

export GOROOT=$HOME/go
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin

Taking net/http for a Spin with Go

Let's start with our usual Hello, World!

// main.go
package main

import (
  "fmt"
  "log"
  "net/http"
)

func main() {
  const port = 8000
  listenAt := fmt.Sprintf(":%d", port)
  http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, "Hello, World!")
  })

  log.Printf("Open the following URL in the browser: http://localhost:%d\n", port)
  log.Fatal(http.ListenAndServe(listenAt, nil))
}

It is a pretty simple server, yet there are quite some details to unpack, especially if you are new to Go.

We are in our main.go file. This is our entry point for compilation at the moment. Calling the file main.go is not necessary, it is merely a convention. However, every Go program needs to have a main package containing a main function.

Imports are imports. If you use any proper IDE, or vim with vim-go, they can handle it for you, so you need not worry about it.

As I mentioned func main is the entry point of our program, just as with most compiled languages you've probably seen. First, we define a const for our port. Then we create a string that would look like ":${port} ".

Go does not support template strings, so we need to use the Sptrintf function of the fmt package.


Standard Go Libraries: Print and friends

Let's stop here for a moment to appreciate how well designed is Go's standards library. The fmt package contains functions similar to C's printf and scanf. But it has fmt.Print, fmt.Sprint, fmt.Fprint, and similar functions with different endings such as fmt.Println and fmt.Printf.

The endings are simple:

  • If there is no "suffix", the function will print strings and stringified values that it receives, separated by spaces.

Eg. fmt.Print("listening at port:", 8000) would result in listening at port: 8000 being printed to the console.

  • The suffix ln means that an ending \n will be added to the string that will be printed.

So in our previous case, if we wanted to print anything after the port, and would like to start a new line for it, we could either do fmt.Print("listening at port :", 8000, "\n"), or fmt.Println("listening at port :", 8000, "\n")

  • Lastly, the suffix f means we can use formatting verbs in our strings.

Thus, we have what we were looking for: fmt.Printf("listening at port :%d\n", 8000) to print listening at port: 8000 followed by a newline.

The prefixes, on the other hand, determine where the string gets printed. The Print family prints to stdout, the Sprint returns the resulting string, while Fprint takes an io.Writer to print into.

The same logic applies to fmt.Scan. It might look unnecessarily verbose to talk so much about printing only, but as you'll probably use these functions a lot both for concatenating and debugging,

it is useful to befriend them as early as possible.


Variables and Constants in Golang

Another thing to pay attention to is that while we define port as a const, using a simple = sign, listenAt is defined as a reassignable variable with :=. If you're familiar with the basics of the language, you'll know that <name> := <value> is a shorthand for var <name> <type> = <value> while inferring its type. The same goes for const, but you can only assign literal values to consts, so the type annotation is not necessary. Why is this important? If you don't pay attention, you'll lose a lot of time not using the right assignment operator.

Also, note that const's andvar's can be defined on top-level as well. Thus it would be more idiomatic in Go to set our port as

// main.go
package main

import (
  "fmt"
  "log"
  "net/http"
)

const port = 8000

func main() {
  listenAt := fmt.Sprintf(":%d", port)
  http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, "Hello, World!")
  })

  log.Printf("Open the following URL in the browser: http://localhost:%d\n", port)
  log.Fatal(http.ListenAndServe(listenAt, nil))
}

So let's recap:

  • var <name> <type> = <value>, eg var port int = 8000 or var listenAt string = fmt.Sprintf(":%d", port)

    • Assigns a value to a new variable
    • Assigning to an existing variable is a compilation error
    • Use simple = for assignment
    • Needs type annotation
    • Can assign literal or function return value
    • Can be present outside of a function
  • const <name> = <value> eg. const port = 8000

    • Assigns a value to a new constant
    • Assigning to existing constant is a compilation error
    • Use simple = for assignment
    • Does not need type annotation
    • Only literals can be assigned
    • Can be present outside of a function
  • <name> := <value> eg. port := 8000 or listenAt := fmt.Sprintf(":%d", port)

    • Assigns a value to a new variable
    • Assigning to an existing variable is a compilation error
    • Use := for assignment
    • Type is inferred
    • Can assign literal or function return value
    • Can only be present inside a function
  • <name> = <value> eg. port = 8000 or listenAt := fmt.Sprintf(":%d", port)

    • Reassigns value of an existing variable
    • Assigning to a new variable is a compilation error
    • Use single = for assignment
    • Type is inferred
    • Can assign literal or function return value
    • Can only be present inside a function

So to make our Hello World app a bit more idiomatic, we could move port out of main.

// ...
const port = 8000

func main() {
  listenAt := fmt.Sprintf(":%d", port)
  // ...
}


The Handler

Our handler should not be surprising. It is quite similar to Node's http.createServer(), with the added benefit that it supports very simple routing out of the box.

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  fmt.Fprint(w, "Hello, World!")
})

However, there are a couple of things to note.

First, our handler takes an http.ResponseWriter, and an http.Request as parameters. Be advised that while in Node.js we got used to writing handler(req, res), in Go, the order of parameters is reversed.

Also note, that the ResponseWriter is passed as a value, while the Request is a pointer. One might expect them to be the other way around, as you probably don't want to mutate the request, but will definitely want to do so with the ResponseWriter. You want to write a response after all! Fear not, it makes sense!

If you take a look at the documentation, http.Request is a struct, while http.ResponseWriter is an interface. It makes sense to pass a pointer to a struct, but if we pass a pointer to an interface, it will point to the interface itself and not the struct that implements it. So bear in mind that if a function takes an interface implementation, it can receive a pointer or a value as well.

In other words: both a pointer and a value can implement an interface.

And finally, we can see fmt.Fprint in action, as it writes to our ResponseWriter. From that, we can also deduce that if something implements the http.ResponseWriter interface, it also implements io.Writer.

At the bottom of the file, we start listening at the port we specified before. It is different from what we are used to in Node.

server.listen(8000, (err) =r {
  if (err) {
    throw err
  }
  console.log('Server is listening at port 8000')
})

In Go, we need to print the message before we call Listen as it will block from then on, as long as the server is listening to requests. Encountered errors are returned instead of passed to a callback.

log.Printf("Open the following URL in the browser: http://localhost:%d\n", port)
log.Fatal(http.ListenAndServe(listenAt, nil))

Here we use log.Fatal to notify us about any returned errors. It calls log.Print then os.Exit(1). As http.ListenAndServe blocks, log.Fatal will only be called, if the server encounters a problem and returns an error, we are fine, our program won't quite right after it starts listening.

The first parameter we pass is the port, and the second is the Handler. If we pass nil, it defaults to http.DefaultServerMux. We will discuss muxers in further detail next time, so for now,

let's just accept that passing nil is fine.


Config and env vars in Go

If we want to adhere to the 12 factor methodology, we'll need to inject our config as environment variables. To do so, let's create a config.go file.

// config.go
package main

import (
  "fmt"
  "os"
  "strconv"
)

type config struct {
  port int
}

func readConfig() config {
  portString := os.Getenv("PORT")

  if portString == "" {
    portString = "8000"
  }

  port, err := strconv.Atoi(portString)

  if err != nil {
    panic(fmt.Sprintf("Could not parse %s to int", portString))
  }

  return config{
    port: port,
  }
}

The first thing to note is that the config is part of package main. This way anything we define here will be available in main.go, so all our types, functions, top-level variables, and consts can start with lowercase letters and will still be accessible in all other files that are part of package main. This might be strange coming from Node's module system, but makes it easier to organize the code across different, but related files,

while reducing the number of necessary imports.


Config in Node.js vs Go

After the package name you can see the imports, then type config struct. In Node, we'd return an object with the parsed configs. The equivalent of this in Go is creating a struct then returning it. Our readConfig function is a bit more verbose than it would be in Node, as it would probably look something like this.

'use strict'

const joi = require('joi')

const portSchema = joi.number().port().default(8000)

const port = joi.attempt(process.env.PORT, portSchema)

module.exports = { port }

With joi we define the schema (the types) and set the default, so the line const portSchema = joi.number().port().default(8000) is equivalent to our struct in and the first if in Go.

type config struct {
  port int
}

func readConfig() config {
  // ...

  if portString == "" {
    portString = "8000"
  }

  // ...
}

Then we read the env var with process.env.PORT and parse it with joi.attempt, which throws an error if the env var is not parseable. To do the same in Go, we need to read the PORT env var, parse it to an int using strconv.Atoi (the name originates from C and stands for ASCII to int). The conversion might return an error, and if it does, we need to panic (throw an error in Node) as the application is in an unknown state from that time on.

func readConfig() config {
  portString := os.Getenv("PORT")

  // ...

  port, err := strconv.Atoi(portString)

  if err != nil {
    panic(fmt.Sprintf("Could not parse %s to int", portString))
  }

  // ...
}

Finally, we export the config object, including the port in module.exports = { port }. As in Go we have packages, we use a function here, which returns our config struct.

func readConfig() config {
  // ...

  return config{
    port: port,
  }
}

Now we can update our main.go accordingly.

// main.go

// ...
func main() {
  conf := readConfig()

  listenAt := fmt.Sprintf(":%d", conf.port)
  http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, "Hello, World!")
  })

  log.Printf("Open the following URL in the browser: http://localhost:%d\n", conf.port)
  log.Fatal(http.ListenAndServe(listenAt, nil))
}


Using Envconfig to Manage Configuration Data from env vars

"Is there a way to make it more concise?" you might ask. Of course!

Let's install Kelsey Hightower's envconfig. He is a pretty big name when it comes to Go and Kubernetes, so let's assume his package can be trusted for now.

First, we need to go get the package.

$ go get github.com/kelseyhightower/envconfig

This downloads envconfig to your $GOPATH/src, so you can take a look at the code at $GOPATH/src/github.com/kelseyhightower/envconfig.

Now we can import it in our config.go

import (
  "fmt"
  "os"
  "strconv"

  "github.com/kelseyhightower/envconfig"
)

Envconfig can automatically read the necessary env vars based on the struct pointer you pass it using envconfig.Process(prefix string, spec interface{}). Prefix can be handy if you want to tag your env vars such as instead of PORT you want to use MYAPP_PORT.

export MYAPP_PORT=8000
type config struct {
  Port int
}

func readConfig() {
  var c config
  err := envconfig.Process("myapp", %c)
}

This also means of course, that the struct fields you wish to populate need to be exported, so we needed to convert the Port field to sentence case.

Another feature is the use of struct tags to specify required env vars or default values for them. But what are struct tags?

Struct tags

Struct tags are a way for us to attach meta information to struct fields. Canonically you set key: "value" pairs enclosed in ""-s and separated by,`-s.

type mytype struct {
  Foo int `tag:"mytag",another`
}

So let's get back to our config file!

Rewriting the config file with envconfig

package main

import (
  "github.com/kelseyhightower/envconfig"
)

type config struct {
  Port int `default:"8000"`
}

func readConfig() config {
  var c config

  err := envconfig.Process("", &c)

  if err != nil {
    panic(err)
  }

  return c
}

Note that the default value in the struct tag is still enclosed in "-s although it is an int.

When calling envconfig.Process, the prefix is an empty string as we will probably deploy it in a container, so no need to separate it from other configs. The variable config is only declared, but not initialized in the first line of readConfig (var c config), and a pointer to it is passed to envconfig.Process. We still need to handle possible errors that might arise when the env vars are parsed, then finally we can return our populated struct.

Don't forget that we still need to rewrite main.go as now Port is exported.

func main() {
  conf := readConfig()

  listenAt := fmt.Sprintf(":%d", conf.Port)
  http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, "Hello, World!")
  })

  log.Printf("Open the following URL in the browser: http://localhost:%d\n", conf.Port)
  log.Fatal(http.ListenAndServe(listenAt, nil))
}

Sidenote: while it is super concise to export values based on the casing, it makes it cumbersome when you use them internally at a lot of places but then realize you need them to be exported. Of course, you can circumvent this by using an editor or IDE which is capable of renaming variables based on the context, but it still would be easier to just add a modifier keyword.

To my mind, this is a drawback of Go, but in the meantime, this practice makes it more visible if a value is exported or not, so it is definitely a matter of opinion.


Making it RESTful with Queries

So far so good, we have our Hello, World! app ready. But to make it RESTful, we need to be able to handle three more things: queries, url parameters, and http methods. Let's start with queries as they are the most simple.

  http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    responseFormat := r.URL.Query().Get("format")
    if responseFormat == "json" {
      w.Header().Set("Content-Type", "application/json;charset=utf-8")
      fmt.Fprintf(w, "{\"foo\": \"Hello, World!\"}")
      return
    }
    fmt.Fprintf(w, "Hello, World!")
  })

First, we use http.Request.URL.Query() to get the query out of the URL. As you can see in the documentation, it returns something with the type Values, which is actually a map[string][]string, so a map of string keys and string slices with some extras such as Values.Get() which will get the first value out of the slice, or an empty string if it doesn't exist, thus saving as a nil check.

We take the query param format and if it equals json, we set the Content-Type to application/json,

send back the response then return.


Encoding JSON in Golang

But writing JSON strings by hand is quite ugly, so let's use the json package to do it for us. Instead, we can create an anonymous struct and encode it as the response.

import (
  "encoding/json"
  "fmt"
  "log"
  "net/http"
)

// ...

  if responseFormat == "json" {
    // ...
    json.NewEncoder(w).Encode(struct {
      Foo string
    }{
      Foo: "Hello, World!",
    })
    return

// ...

We create a NewEncoder and pass it our http.ResponseWriter, then call Encode on the returned Encoder and pass it our struct. Anonymous structs are declared the same way as any other structs: between {}-s and you declare each field, and its respective type. The only difference is that here, we need to instantiate it immediately.

struct {
  Foo string
}{
  Foo: "Hello, World!",
})

Whereas the ones you might have encountered before look more like:

type Hello struct {
  Foo string
}

func main() {
  hello: := Hello{"Hello, World!"}
  fmt.Println(hello.Foo)
}

Also note, that similarly to envconfig.Process, anything you wish to be serialized into JSON must be exported, thus has to start with an uppercase letter, as otherwise, the Encoder won't be able to access it. However, if you try and call the server now, you'll get {"Foo": "Hello, World!"}, which doesn't look like the JSON-s we're used to seeing. We can specify how we want our fields to be encoded using a json struct tag.

import (
  "encoding/json"
  "fmt"
  "log"
  "net/http"
)

// ...

  if responseFormat == "json" {
    // ...
    json.NewEncoder(w).Encode(struct {
      Foo string `json:"foo"`
    }{
      Foo: "Hello, World!",
    })
    return
  
// ...

Finally, our main.go should look like this:

package main

import (
  "encoding/json"
  "fmt"
  "log"
  "net/http"
)

func main() {
  conf := readConfig()

  listenAt := fmt.Sprintf(":%d", conf.Port)
  http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    responseFormat := r.URL.Query().Get("format")
    if responseFormat == "json" {
      w.Header().Set("Content-Type", "application/json;charset=utf-8")
      json.NewEncoder(w).Encode(struct {
        Foo string `json:"foo"`
      }{
        Foo: "Hello, World!",
      })
      return
    }
    fmt.Fprintf(w, "Hello, World!")
  })

  log.Printf("Open the following URL in the browser: http://localhost:%d\n", conf.Port)
  log.Fatal(http.ListenAndServe(listenAt, nil))
}


Tidying it Up

We have our very basic Hello World app up and running. However, it might not be too tidy. That's the point where you might start thinking about adding a linter to your code.

But the problem with linters is that you can spend days or even weeks arguing about things that don't matter as much, such as whether to use ;-s in JavaScript (who cares?) or whether to leave spaces between function names and parentheses or not (boy, not this again!).

Luckily, this is not a problem when you write Go as it ships with its own prettifier called go fmt, and there is an overwhelming consensus to use them as they come out of the box and be done with it. Even better, most IDEs and editors support them as well, so for example in vim, if you use vim-go it will run go fmt on your files automatically when you save them. The same is true for VSCode if you turn on the formatOnSave option, and you can achieve the same in Goland as well using watchers.

In vim, you can use this to your advantage as if you setup autosave as well, you just need to type your code and when exiting insert mode, your code will be automatically formatted, and your imports will be up to date.

call plug#begin('~/.vim/bundle')

Plug 'fatih/vim-go', { 'do': ':GoUpdateBinaries' }
Plug 'vim-scripts/vim-auto-save'

call plug#end()

" vim-go
" ======

let g:go_fmt_command = "goimports"

" Autosave
" ========

let g:auto_save = 1
let g:auto_save_in_insert_mode = 0


Dependency Management with go.mod

Since v1.11, Go also ships with its own dependency management tool called go mod. It is the default way of building sources outside of one's GOPATH or if you set the env var GO111MODULE=on. It can help a lot when you need to build your code on your CI/CD server or wish to distribute some code without the need for others to create Go's canonical directory structure for themselves.

To get started, we'll need to set the env var properly.

$ GO111MODULE=on

$ go mod init
go: creating new go.mod: module github.com/RisingStack/go-gorilla-example

$ GO111MODULE=auto

The command go mod init creates a go.mod and go.sum file, which contains the list of our dependencies, the Go version we're using and a cryptographic checksum of the downloaded dependencies. This way if you try to build your outside your GOPATH or on a different machine with Go installed, it will automatically download the dependencies for you. You can read more on modules in the Go wiki or running go help modules in your terminal. When you're done, don't forget to set the GO111MODULE env var back to auto.

Otherwise, you might see go get and the overall build behaves strangely when you're inside your GOPATH.


Sneak Peek to our next Golang Tutorial

This has been quite a long introduction just to write a single "Hello, World!" app, but hopefully, this detailed explanation makes it easier to wrap your head around what is happening when you run your code.

Stick around as next time we'll continue with adding more routes and setting up a database connection with as much detail as possible too.

Feel free to discuss the post here:

Learning Go as a Node developer from node