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
You should now see go.mod
with the name of the project and Go version:
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)
}
}
To run the server type in terminal:
go run .
You should see something like that:
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
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:
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"
)
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(){
/** **/
}
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
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"
Next up we are logging the full server address:
log.Printf("Server is listening on: http://%v", address)
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")
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)
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)
}
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>`))
}
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)
}
}
Stop server with ctrl+c
and start it again.
You should see a huge header in your browser now:
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)
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) {
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>`))
We take this (stripped) HTML page text:
<html><body><h1>I AM GROOT!</h1></html>
Turn text into a slice (sequence) of bytes:
([]byte)(some_text)
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)
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 .
You'll see new file called web-server
.
Unix/MacOS users need to allow its execution with:
chmod +x web-server
Otherwise you can run it like any other program.
For Unix:
./web-server
Or, for windows:
.\web-server.exe
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 .
You are only allowed to make changes to the main.go
file though!
See you next time! And good luck!
Top comments (1)
Very clear, thanks!