This introduction to Go is geared towards anyone, with
- some development background (what are variables, what are loops, conditions, functions, etc …)
- a running Go development environment (
go
command line tooling + an IDE of your choice) - an interest in learning Go
If you don’t have a running Go development environment yet, please check out the previous article of this series.
Outcome of of this article will be:
- Understanding of the Go language basics
- Ability to read & write simple Go code
- Some experience with Go CLI (run, compile)
- Some experience with Go standard lib
- Know about (official) Go documentation and other useful sources
This part will prepare you for the next part in the series: Intro to application development in Go.
A hello-world HTTP server
As this is a show-and-tell introduction let’s first have a look at the code of a small, working Go program. I will guide you through the lines so that you can become familiar with the essentials on the way.
Without further ado here is a minimal HTTP server:
// returns a `200 OK` response with the body `Hello World` to any incoming request
package main
import (
"fmt"
"net/http"
)
const address = "127.0.0.1:12345"
var counter = 0
func main() {
fmt.Println("Starting HTTP server at", address)
http.HandleFunc("/", helloWorld)
err := http.ListenAndServe(address, nil)
if err != nil {
panic(err)
}
}
func helloWorld(writer http.ResponseWriter, request *http.Request) {
counter++
response := fmt.Sprintf("Hello World (%d)", counter)
writer.Write([]byte(response))
}
You can find all the Go code also in http://github.com/ukautz/go-intro
Run Go code
Copy the above code and store it in a filed named main.go
(wherever you prefer on your file system). Then open up a terminal, navigate to the directory the file is stored in, then execute:
$ go run main.go
Starting HTTP server at http://127.0.0.1:12345
Once that happened, you can visit the URL http://127.0.0.1:12345
either in your browser or with curl
(from another terminal). In any case, you should see: Hello World (1)
with increasing numbers in parenthesis with each request.
Congratulations! You ran your first Go HTTP server ๐ ๐
Hint: You can stop your HTTP server from the terminal by hitting ctrl + c
.
Compile into binary
While we are at it, let me show you how to compile your HTTP server into a binary file, so that it can be deployed to other systems (which do not have the go
compiler installed) and executed there.
The difference is minimal. Instead of go run
you execute go build
, like:
$ go build main.go
This will create a binary, executable file named main
in the same directory, which you can then execute as any other executable file:
$ ./main
Starting HTTP server
Hint: The file name main
is automatically derived from main.go
. You can specify an alternative with the -o <name>
parameter, as in: go build -o http-server main.go
Understanding the code
Now that you saw it working, let us go through the code block by block:
Writing code comments
Let’s start out easy, albeit with a very important feature of any language: The code comment. The first line in the example program reads as following:
// returns a `200 OK` response with the body `Hello World` to any incoming request
This is a comment in Go. For multiline comments you can also use the /**/
as in:
/*
This is a
multiline
comment
*/
Declaring the package
Continuing with the next line:
package main
Go orders code into packages. The first line of code in every .go
file is always the package <package-name>
declaration (aside from comments or whitespace).
The package name should be concise and is by convention a lower case, single-word name, without underscores or mixedCaps. Basically: keep it short & simple.
The package name main
which is used here is special: In Go all execution starts in the main
package. Since we are writing a simple, executable HTTP server, all the code can live in a single file: the main.go
. With larger projects, you would structure your code into multiple files, which would live in multiple directories and thereby multiple packages.
Note: Every .go
file within the same directory must be in the same package. If you have multiple .go
files in a directory and not all have the same package <name>
, then Go will refuse to compile! Also: Usually, the package name matches the name of the directory the file is in. However, this is a convention and can be broken when necessary (more on that in the next article of this series).
Importing other packages
import (
"fmt"
"net/http"
)
As mentioned before Go structures code into packages. To use any code from another package you first need to explicitly import the package. The above code block imports the two packages fmt
and net/http
- from the Go standard library to be precise. The standard library is part of the Go SDK and comes with Go itself - no need to install it separately.
Note: Packages from the standard library have very short import names. You will also use 3rd party libraries (=modules), which are often hosted on public repositories, and which have a longer name of the form <public-location>/<repository>
, as in github.com/aws/aws-sdk-go
- more on that later.
Package documentation
You can investigate all publicly available Go packages using the https://pkg.go.dev
website (formerly known as https://godoc.org
). Just visit: https://pkg.go.dev/<package-path>
to display the documentation of the exported interface of the package.
In practical terms, here is the documentation for the two packages we imported above:
Note: as this documentation is auto-generated from comments in the source code, this also works with 3rd party packages, like the aforementioned AWS SDK: https://pkg.go.dev/github.com/aws/aws-sdk-go. Of course it would not work with privately hosted (access restricted) packages.
Constants & Variables
const address = "127.0.0.1:12345"
var counter = 0
Most higher level languages differentiate between constants and variables. So does Go. In the above code snippet you see first the declaration of a string
-type constant named address
then the declaration of the int
-type variable named counter
.
As the name implies a constant is unchangeable (immutable) after declaration, whereas a variable can be modified (mutated) at leisure. To make that clear:
// declare a variable with an initial value
var iAmChangeable = "foo"
// change the value of the variable -> no problem
iAmChangable = "bar"
// declare constant with value
const iAmUnchangable = "foo"
// try to change constant after declaration -> failure at compile time
iAmUnchangable = "bar"
However, while you can change the content of a variable after declaration, you cannot change the type! Which means the following will fail:
// declare a string variable with initial value
var iAmChangeable = "foo"
// attempt to assign an int value to variable after declaration -> failure at compile time
iAmChangeable = 123
Same as with the import
code from above, Go knows two notations. The following is equivalent and makes sense when more than one constant (or variable) needs to be declared:
const (
address = "127.0.0.1:12345"
)
var (
counter = 0
)
About types
Go is a strong typed language. This means: every constant or variable is of a specific, unchangeable type.
The type of a variable or constant can be provided implicitly (as above) or explicitly. The above could be written explicitly like so:
const address string = "127.0.0.1:12345"
var (
counter int = 5
)
As Go is perfectly able to figure out the types in the above example, I omitted them. However, this works for a small set of types, namely: string
, int
, float64
and bool
. Here an example of types Go can “auto detect”:
var aString = "123"
var anInt = 123
var aFloat = 123.456
var aBool = true
There are, roughly speaking, two “kinds of types”:
built-in (primitive) types
- like
int
,uint
,int64
,float64
,string
,byte
,bool
etc as you might now from other languages - they are written in
lowercase
- in depth: go101.org - Basic Types and Basic Value Literals
user defined (complex) types
- they are specified in plain Go in the standard library, 3rd party or your code
- they look like
package.CamelCase
- the next post in this series will elaborate in depth on them
Function declarations
Going back to the above main.go
file, let’s have a look at the main
function therein:
func main() {
fmt.Println("Starting HTTP server at", address)
http.HandleFunc("/", helloWorld)
err := http.ListenAndServe(":12345", nil)
if err != nil {
panic(err)
}
}
Ok, this is a bigger piece of code, let’s go through it slowly:
func main() {
// ..
}
This is a minimal declaration of a parameter-less function which does not return anything.
The general pattern for functions in Go looks as following:
func nameOfTheFunction(param1 int, param2 string) (string, error) {
// body
}
To put that in words:
- starting with the keyword
func
- followed by the
nameOfTheFunction
- followed parenthesis
()
which may contain a list of typed input parameters- here:
(param1 int, param2 string)
, meaning that the function call expects two parameters, the first of the typeint
and the second of the typestring
- here:
- optionally followed by typed return parameters (within parenthesis
()
- if there is more than one)- here
(string, error)
, meaning that the function call will return two variables, the first of the typestring
and the second of the typeerror
- here
- followed by the body (code) within the curly brackets
{}
The main function is special
The main
function, with an empty signature, within the package main
is a special case. It indicates to Go that this function is an entry-point for execution. Basically what is being called when running go run
from the shell (or executing the compiled binary).
Having a main
function in any other (named) package has no consequence at all. Only in the main
package it becomes a special case.
Using packages
Let us investigate the the first line of the body of our main
function in detail:
fmt.Println("Starting HTTP server at", address)
You can probably guess what this line is doing: It prints out Starting HTTP server at 127.0.0.1:12345
(on STDOUT
, to be precise). The prefix fmt.
refers to the previously imported fmt
package. Println
is a public (exported) function from this fmt
package.
Access modifiers (public / private)
You might have noted a difference in spelling of our functions (main
and helloWorld
) and that Println
function from the fmt
package. The uppercase P
is intentionally, as this is how you specify in Go the accessibility (visibility) of, well, anything. In this case a function, but this goes for types, constants, variables etc.
To be clear, the difference is:
- private things can only be accessed (e.g. called) by other code within the same packages (i.e. from files in the same directory)
- public things can be accessed by any code that imports the package it lives in
Or even shorter: Only Uppercase is part of the public interface of a package.
Setup HTTP routing
Coming back to the next line of code in the main
function:
http.HandleFunc("/", helloWorld)
Go comes with a very powerful HTTP server (and client) out of the box. It is highly configurable and allows you to run anything from a simple hello world to a high-load, high-concurrency, next-facebook-like application.
Luckily, most of the complexity that this entails is hidden until you need it. There are two things you have to do to run a simple web server in Go:
- Tell Go what do with incoming requests (aka routing)
- Tell Go on what address to start the HTTP server on
The HandleFunc
function from the http
package handles the first part: configures the routing of the HTTP server.
For that it requires:
- a path prefix: the chosen
/
is the root of all paths and thereby accepts any (requested) path - a handler function:
helloWorld
is our handler, we’ll get to it soon
Note: Functions can be passed as arguments the the same way as variables or constants can.
Start the HTTP server
Next up in the main
function:
err := http.ListenAndServe(address, nil)
This call to the public function ListenAndServe
of the net/http
package starts an HTTP server and binds it to the provided address.
The first input parameter address
(above declared as a constant with value: 127.0.0.1:12345
) provides the address the HTTP server must listen on.
The second input parameter nil
is commonly seen in Go. This is equivalent to null
in many other languages and is an “empty value” for many (non-primitive) types in Go. As ListenAndServe
supports multiple use-cases, it needs that second, optional parameter. We don’t need that option here and hence can (or rather have to) provide nil
.
The return parameter err
(of the type error
) will only be returned if the listening and serving fails. This could happen, for example, if the port 12345
is already in use on the address 127.0.0.1
. If no error occurs then ListenAndServe
will block and keep running until you abort it (i.e. hit ctrl + c
).
Error handling
The last code block in the main
function will soon become a common occurrence to you:
if err != nil {
panic(err)
}
Every language has their own way do deal with errors at runtime. Many do exception handling, which delegates runtime errors until something “catches” them (like throw <error>
and try { something } catch <exception> { handle }
).
Go does not have any of that. Instead it solves error handling by convention. This convention is to return any error that happens in the function and then appropriately handle the error in the caller.
Hence you will very often find the if-err-return
pattern like so:
func SomeFunction() error {
value, err := someCall()
if err != nil {
return err
}
}
Now, the caller of SomeFunction
must take care to delegate the error
“further up” as needed. Usually this means the error is handed up and up, until someone writes a main
function and uses panic
to act on the error, as we are doing in the example.
The panic
function is one of the few built-in functions. It prints out a message (here: the error) then exits the program with a non-zero exit code. Depending on compilation it also prints a stack-trace from the panic location, so that developers - like you - can roll up our their sleeves and dig into debugging with a good idea where to start.
There is also a more condensed form of the if-err-return
pattern, which looks like:
if err := anotherCall(); err != nil {
return err
}
HTTP handler function
This last piece of code implements our HTTP handler function, which was associated earlier with http.HandleFunc()
call:
func helloWorld(writer http.ResponseWriter, request *http.Request) {
counter++
response := fmt.Sprintf("Hello World (%d)", counter)
writer.Write([]byte(response))
}
In short what is does:
- increment the global variable
counter
by 1 - render a response string
Hello World (<counter>)
- print the HTTP response
The function input parameters refer to custom types declared in the http
package. As we are using this function as an argument for http.HandleFunc
above it must have this exact form:
func handler(http.ResponseWriter, *http.Request) {
// body
}
Note: The example code is not concurrency-safe. If you were to send requests to our HTTP server in parallel, it would eventually die and exist with a panic
, since concurrent attempts to increment counter
and / or read from counter
would fail. This can be easily safe-guarded in Go, but I chose not to do it here for the sake of readability and brevity. If you feel like exploring, have a look at the sync
or sync/atomic
packages in the standard library.
Calling methods on instances of types
writer.Write([]byte(response))
The above expression in the body of the helloWorld
method shows how a method of a type (=http.ReponseWriter
) is called on the instance of a type (=writer
).
It would be pronounced like so: calling the Write
method on the writer
variable, which is an instance of the http.ResponseWriter
type.
While this looks somewhat similiar to the http.HandleFunc
and http.ListenAndServe
called in the main
function, the difference is that the Write
call is “executed” on an instance (writer
), not a package (http
). It’s a method, not a function.
Without going into too much detail (more on that in the following article of this series), know that custom types can have methods. You can then call these methods on the instances of those custom types. As in many other languages.
To investigate what methods are available for a type, you can again use pkg.go.dev
and find out. In the case of the http.ResponseWriter
have a look here:
Type conversion
[]byte(response)
This expression shows a type conversion. The inner part (response
) is a string
-type variable. As with in many other languages, strings and byte-arrays are closely related. Hence Go allows you to convert the string on-the-fly into []byte
(which Go calls slice of bytes, not byte array).
The conversion needs to be done because the Write()
method expects a single []byte
input parameter and not a string
.
Go allows you to convert from and to various built-in types. For example:
int(2.55)
- converts the float number value2.55
into an integer value (which changes the value to2
of course)uint8(5)
- converts the value of5
(which Go assumes asint
) into an unsigned integer with the (bit) length of 8string([]byte("foobar"))
- converts the string value “foobar” into a byte slice and back into a string (which is of course redundant, but maybe helps as an illustration)
Summary
Ok, this was quite a bit. You can find the full source code of everything above here: https://github.com/ukautz/go-intro/tree/master/beginner/http
A quick review on what you learned:
- Running and compiling Go code
- Lookup Go packages at https://pkg.go.dev
- The language basics of Go:
- Commenting code
- Create and import packages
- Using constants and variables
- Creating and calling functions
- Using and casting types
- Dealing with errors
- Writing an HTTP server
- Routing to an HTTP handler
- Writing an HTTP response to the requestor
If you are curious, check out another small command line application, which creates a table of content (toc) for Markdown files: https://github.com/ukautz/go-intro/tree/master/beginner/mdtoc
Next up
Learn writing full fledge command line applications along with diving deeper into the Go language in the last article of this series.