DEV Community

Valeria
Valeria

Posted on • Edited on

Introduction to Go & WebDev

Update: Initially I started this series for strictly educational purposes, but in the process I accidentally got an idea that grew into something bigger and I'm no longer able to provide source code for the project. If you're looking for pointers regarding development tooling, take a look at Encore. I kept the introductory articles and hopefully you'll find them useful.

Today you are going to setup your development environment and build a minimalistic web server in Go.

Sounds interesting? Let's get started then.

Grab the tools

Every self-respecting developer has a code editor 😎.
If you don't have one yet, go ahead and grab VSCode. There are others, of course, but this one is good enough and free enough πŸ˜‰

I mentioned that you'll be learning Go. That's a very powerful, yet easy to grasp language. And it has been widely adopted by the giants of tech industry so you won't have issues finding a juicy gig too πŸ€‘

If you haven't installed Go compiler yet, go ahead, download it from go.dev & install.

And last, but not least, you'll need a Go plugin for your code editor. If you're using VSCode get this Official Go Plugin.

All set? Nice job πŸ‘

The First Web Server

You'll need some descriptive name for your first project: something nice but short, like web-server.

Open your code editor and then a new terminal within the editor. In VSCode you can do it by selecting "Terminal" -> "New Terminal" from the top menu.

To create a new Go project, type these commands in the terminal:

mkdir web-server
cd web-server
go mod init web-server
Enter fullscreen mode Exit fullscreen mode

You should now see go.mod with the name of the project and Go version:
go.mod module web-server go 1.17

Create a file called main.go by the side of go.mod file:

package main

import (
    "log"
    "net/http"
)

func main() {
    address := "localhost:8080"
    log.Printf("Server is listening on: http://%v", address)
    err := http.ListenAndServe(address, nil)
    if err != nil {
        log.Fatal(err)
    }
}
Enter fullscreen mode Exit fullscreen mode

To run the server type in terminal:

go run .
Enter fullscreen mode Exit fullscreen mode

You should see something like that:
2022/01/05 18:57:23 Server is listening on: http://localhost:8080

If you see this error instead:

2022/01/05 19:03:33 Server is listening on: http://localhost:8080
2022/01/05 19:03:33 listen tcp 127.0.0.1:8080: bind: address already in use
exit status 1
Enter fullscreen mode Exit fullscreen mode

Change the port number in the address to another value, e.g. 8081, and try again.

If you navigate your browser to http://localhost:8080 (or the address you're using), you'll see:

404 page not found

It may not seem like one yet, but this thing that you've just made is a real HTTP server.

We'll dive deeper into the protocol itself further down the road, for now let's blindly trust net/http package on it 😁

The main.go file starts with package main. Every Go file in the root of this directory should start exactly the same way. And, expectably, nested packages should be located in nested directories and called accordingly.

Next, we are importing two packages from the standard library:

import (
    "log"
    "net/http"
)
Enter fullscreen mode Exit fullscreen mode

This tells the compiler to include code from these packages in addition to the code in the main.go file.

And then the main function. You can have other functions here too, for example:

func aFunction(){}
func anotherFunction(){}
func main(){
/** **/
}
Enter fullscreen mode Exit fullscreen mode

But, unless you call other functions from inside main, they won't be executed. So func main in a main.go file is the entry point for any executable project.

And, finally, the contents of the main function begin with a variable called address, that we are assigning the value of localhost:8080.

If you hover over the variable name in your code editor, you'll see a hint like this:

var address string
Enter fullscreen mode Exit fullscreen mode

That's because operator := tells the compiler to infer a type based on its value. You could explicitly define the variable and assign a value to it too, works either way:

var address string = "localhost:8080"
Enter fullscreen mode Exit fullscreen mode

Next up we are logging the full server address:

log.Printf("Server is listening on: http://%v", address)
Enter fullscreen mode Exit fullscreen mode

To do so we are calling a Printf function from the log package. The first parameter is a string with a placeholder %v, which means use the default format. And the next parameter is what we want to replace the placeholder with.

So if we'd want to format multiple things we would add multiple placeholders:

log.Printf("Server is listening on: http://%v:%v","localhost","8080")
Enter fullscreen mode Exit fullscreen mode

It's an easy way to compose strings and if you want to learn more, take a look at fmt package documentation.

Finally we got to the most important line:

err := http.ListenAndServe(address, nil)
Enter fullscreen mode Exit fullscreen mode

As the name suggests, it calls ListenAndServe function from the net/http package and passes string address and a nil as arguments.

We don't really need the second argument, but in Go if a function requires an argument - you should pass an argument, you can't just skip it. And nil means nothing in this case. Literally.

Function ListenAndServe returns an error, e.g. if it fails to bind the port. When there is no error its value will be nil, that's why we check for it:

if err != nil {
  log.Fatal(err)
}
Enter fullscreen mode Exit fullscreen mode

And, if so happens that there was an error, we log it and shut the server, that what log.Fatal does.

Phew, that was as long one. I'm glad you made it this far πŸ˜…

Serving Content

You've built a web server, but it doesn't do anything.
How about we make it respond with something more interesting?

Add a new function to main.go:

func HomePage(w http.ResponseWriter, req *http.Request) {
    w.Write(([]byte)(`<html><body><h1>I AM GROOT!</h1></html>`))
}
Enter fullscreen mode Exit fullscreen mode

And add a line before ListenAndServe call inside the main function:

func main() {
    address := "localhost:8080"
    log.Printf("Server is listening on: http://%v", address)
    http.HandleFunc("/", HomePage) // <- Add this line
    err := http.ListenAndServe(address, nil)
    if err != nil {
        log.Fatal(err)
    }
}
Enter fullscreen mode Exit fullscreen mode

Stop server with ctrl+c and start it again.
You should see a huge header in your browser now:
I AM GROOT

As you've probably guessed, we added a handler to the server and pointed out that we would like to process all the requests, starting with the path / with the HomePage function:

http.HandleFunc("/", HomePage)
Enter fullscreen mode Exit fullscreen mode

Now the HomePage function needs to have proper type, that means the arguments and return values need to be exactly the way http.HandleFunc wants it to be, and therefore we ended up with the following signature:

func HomePage(w http.ResponseWriter, req *http.Request) {
Enter fullscreen mode Exit fullscreen mode

The first parameter is, as the name suggests, a response writer and the second one is a pointer to the request that we are processing. Pointer types start with * and mean that they are references to values, not the values itself.

The actual work happens inside the body of the HomePage:

w.Write(([]byte)(`<html><body><h1>I AM GROOT!</h1></html>`))
Enter fullscreen mode Exit fullscreen mode

We take this (stripped) HTML page text:

<html><body><h1>I AM GROOT!</h1></html>
Enter fullscreen mode Exit fullscreen mode

Turn text into a slice (sequence) of bytes:

([]byte)(some_text)
Enter fullscreen mode Exit fullscreen mode

The parenthesises around the byte[] are there just to wrap the type, since it has the [] part.If we'd want to convert the other way around we'd do:

string(some_bytes)
Enter fullscreen mode Exit fullscreen mode

And finally, we send the page to the client, that initiated request, by calling w.Write.

Woohoo! We built a server! πŸ₯³

But did we build a server?

Ooops, you're right, we have been using go run so far, but go compiles to an executable program with ease!

Stop the server (ctrl+c) and type in terminal:

go build .
Enter fullscreen mode Exit fullscreen mode

You'll see new file called web-server.

Unix/MacOS users need to allow its execution with:

chmod +x web-server
Enter fullscreen mode Exit fullscreen mode

Otherwise you can run it like any other program.

For Unix:

./web-server
Enter fullscreen mode Exit fullscreen mode

Or, for windows:

.\web-server.exe
Enter fullscreen mode Exit fullscreen mode

Good job! You've made it till the end! πŸ‘

Bonus Challenge

Hungry for more? Here's a challenge for you πŸ’ͺ:

Take this unit test file:

Place it by the side of main.go and make sure it passes when you run:

go test .
Enter fullscreen mode Exit fullscreen mode

You are only allowed to make changes to the main.go file though!

See you next time! And good luck!

Buy me a coffee

Top comments (1)

Collapse
 
leonardoromero profile image
Leo Romero

Very clear, thanks!