Why Go?
I often say that Go found its way to me. When version 1.22 was released, I was already deeply immersed in TypeScript for both frontend and backend development. Despite having bookmarked several articles about Go, I hadn't found the time to dive into them. I can't pinpoint exactly what made me fall in love with Golang, but does it even matter? There's a "Why Go?" page on their website if you need convincing.
Fast forward five months, and I've been writing Go inconsistently. I've taken breaks to satisfy my craving for JavaScript, only to find myself frustrated with its toxic community and superiority complex over PHP and other languages (though perhaps it's just my Twitter timeline that's filled with that). Despite these detours, I've managed to grasp the basics of Go. They say the foundation matters, and I believe that's true.
Well, that's enough chit-chat. Reading is the best way to learn anything in the tech space, so I picked up "Learning Go: An Idiomatic Approach to Real-World Go Programming" by Jon Bodner, and it's been smiles ever since. Although I'm not finished with it yet, I already feel good and confident about my understanding of Go basics. A good friend also recommended "Concurrency in Go" by Katherine Cox-Buday.
Why am I sharing these? Well, I thought you might be one of us—a member of the JS community where multithreading isn't really a thing(It is, but with Web Workers). Instead, you "async" your way out of every problem without necessarily grasping the underlying concepts. I guess you'll await understanding as a promise (bad joke, really). In Go, concurrency is a big deal with Goroutines. Goroutines are a fundamental part of Go's design, making concurrency more accessible and efficient compared to traditional multithreading approaches.
Though I'm not going to implement Goroutines and WaitGroups, it was worth mentioning. With every release, Go gets better. It simply keeps things simple. I'll be implementing a simple API server from scratch with vanilla Go. While there are frameworks like Gin, the main focus here is to show you how easy it is to implement an API in vanilla Go.
Just to throw this in: if you're coming from C or another low-level language, concepts like pointers and references should be familiar territory.
Writing a simple API in Go
I'm going to try to keep it simple. First, you'll want to download Go from their official website. The current version at the time of writing is 1.22.4. Follow the instructions in the manual to install Go on your local machine.
To verify your installation, open a terminal and run the following command:
$ go version
This command prints the version of Go you have installed, confirming that Go is successfully set up on your PC.
Next, choose any text editor or IDE of your choice—VS Code, IntelliJ, GoLand, etc. Make sure to install the necessary Go plugins for your chosen editor, and you'll be ready to code along.
Open a new project in an empty directory. Then, open a new terminal and create a new Go module using the go mod init
command. Run it with your module path, for example, example.com/your-name
.
If you plan to publish a module, this path should be one from which your module can be downloaded by Go tools. However, since we are not creating a Go library/module today, you can simply use your GitHub account path. I’ll use:
go mod init github.com/ebarthur/golang/api
You should see a go.mod
file in your project directory. This file is similar to package.json
in JavaScript projects, as it tracks the dependencies for your Go module.
Now, let's create a main.go
file. But before we dive into this, let me introduce you to Go packages:
In Go, a package is a way to organize and group related Go files. Each Go file starts with a package declaration, which specifies the package to which the file belongs. For instance, if you have a file main.go
, it typically starts with:
package main
The main
package is special in Go. It tells the Go runtime that this package should be compiled as an executable program rather than a shared library.
The func main
is the entry point of a Go program. When you run your program, the Go runtime starts executing from the main
function. Here’s a simple example of a main.go
file with a func main
:
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
You can have multiple packages in a single Go project, which allows for modular code organization and reuse. However, for simplicity, we will focus on using just one package: main
. Now, let's create an api.go
file. You are ready to code with me.
Step-by-Step Guide to Writing api.go
Step 1: Setup Your Package
At the top of your api.go
file, declare the package and import the necessary libraries:
package main
import (
"log"
"net/http"
)
package main
: Defines the package asmain
, indicating this is the entry point of the program.Imports: Import
log
for logging andnet/http
for HTTP server functionality.
Step 2: Define the APIServer Struct
Create a struct to represent your API server. This struct will store the server’s address:
// APIServer is a struct for every HTTP server we will create
type APIServer struct {
addr string
}
APIServer
Struct: Holds the server address.
Step 3: Create a Constructor Function
Define a function to initialize a new APIServer
instance. This function takes an address as a parameter and returns a pointer to an APIServer
:
// NewAPIServer is a factory for creating an instance of APIServer
func NewAPIServer(addr string) *APIServer {
return &APIServer{
addr: addr,
}
}
NewAPIServer
Function: Initializes and returns a new server instance with the specified address.
Step 4: Implement the Run Method
Add a method to start the server. This method sets up routes and starts the HTTP server:
// Run method starts the server
// It is a method available to any APIServer instance
func (s *APIServer) Run() error {
router := http.NewServeMux()
// We return a path value in the request url in this endpoint
// @GET /users/{userID}
router.HandleFunc("GET /users/{UserID}", func(w http.ResponseWriter, r *http.Request) {
// Get PathValue
userID := r.PathValue("UserID")
// Response Status: 200(OK)
w.WriteHeader(http.StatusOK)
// Return UserID
w.Write([]byte(userID))
})
// Set server parameters
server := http.Server{
Addr: s.addr,
Handler: router,
}
log.Printf("Server is listening at %s", s.addr)
return server.ListenAndServe()
}
Router Setup: Use
http.NewServeMux()
to create a new multiplexer.Route Definition: Handle GET requests at
/users/{UserID}
.Server Configuration: Set server address and handler.
In the main.go
file, we can start our server to test it:
package main
import (
"log"
)
func main() {
// Run server at address :4032
// You can choose any address that you want
server := NewAPIServer(":4032")
err := server.Run()
// Since the Run method in `api.go` returns an error, we have to handle it
if err != nil {
log.Fatal(err)
}
}
Notice how we didn’t need to import NewAPIServer
in main.go
—it was available automatically. This is because main.go
and api.go
are part of the same package. If api.go
were in a different package, we would have had to import it explicitly. Learn more about Go packages and imports here.
Start your API in the terminal:
go run .
The server should be up and running if you did everything right in this tutorial:
2024/06/16 19:03:25 Server is listening at :4032
Time to test it. You can use Postman, ThunderClient(a Vs Code extension), cURL or simply type the server address in your browser. I will use cURL.
$ curl http://localhost:4032/users/12
This should return the number 12
. Why? Go back to api.go
and check for yourself. We extract the userID from the URL path and return it. Simple right? Hehe!
We can simply call it a day but we can do more. We can even add middleware. A middleware is a function or a chain of functions that processes HTTP requests and responses. Middleware functions sit between the HTTP server and the final request handler (or route handler). They can modify the request, inspect or alter the response, handle errors, or perform other tasks before passing control to the next middleware or the final handler in the chain. We can create a logging middleware and maybe simulate an authentication middleware. Just stick with me.
If we used Gin, we would get a logger by default but since we are using Vanilla Go, we don't get it out of the box. Let's create one for ourselves In api.go
, let's create a Request Logger middleware:
func RequestLoggerMiddleware(next http.Handler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log.Printf("METHOD: @%s\nPath: %s", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
}
}
We can then wrap our route inside this middleware so that with every request we get a logger in the console.
// Set server parameters
server := http.Server{
Addr: s.addr,
// Handler: router,
// Edit the code and wrap the middleware inside the router
Handler: RequestLoggerMiddleware(router)
}
Now, if we stop and rerun our server (Remember how we start a server?) and make the same request again.
$ curl http://localhost:4032/users/12
The terminal should print a beautiful logger:
2024/06/16 19:29:44 METHOD: @GET
Path: /users/12
It gets even better! What if we want to protect our route. We only want to give access to our users. EMPLOYEES ONLY!
We can simulate a token auth similar to JWT. Here is how:
// RequireAuthMiddleWare checks for a valid authorization token
func RequireAuthMiddleWare(next http.Handler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// We check the request header for Authorization
token := r.Header.Get("Authorization")
// Only allow users with "Authorization: Bearer Token" in their request
if token != "Bearer token" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
}
}
Now, we can chain our middleware. We will wrap our route handler and the Request Logger inside RequireAuthMiddleWare
:
// Set server parameters
server := http.Server{
Addr: s.addr,
// Handler: router,
// Edit the code and wrap the middleware inside the router
Handler: RequireAuthMiddleWare(RequestLoggerMiddleware(router))
}
Let's stop our server and rerun. Send a request to the same endpoint:
$ curl http://localhost:4032/users/12
Unauthorized
Oops! What happened? Did we lock ourselves out of our own server? I think we did lol. So basically, our server is saying that we do not have the permission to access that endpoint. Let's show our employee tag to our server then:
$ curl -H "Authorization: Bearer token" http://localhost:4032/users/12
12
And it worked! The middleware checked our request header and our token matched so it gave us a go-ahead. Now that was fun.
This is very basic and we can even dive deeper, where we can explore other request methods, find a better way to chain middleware, and even connect to a database. We can even implement route-nesting or sub-routing but maybe another day. This tutorial is found in one of the many folders where I log almost every little thing I learn on Go. Find the complete project here.
Conclusion
In this guide, we’ve taken a step-by-step journey through setting up a basic API server in Go. Starting from the installation of Go to writing a simple API, and incorporating middleware for logging and authentication, we’ve covered the essentials to get you up and running with Go. The process was designed to be simple and educational, emphasizing the beauty of writing clean, maintainable code in Go.
Using Go's built-in features and some custom middleware, we’ve seen how easy it is to build and secure a basic API. The example of chaining middleware functions, from logging to authentication, showcases Go’s powerful and flexible design. Whether you’re coming from a low-level language background or a more high-level environment like JavaScript, Go’s concepts like pointers and references should feel like a smooth transition.
Feel free to check out the complete project and while at it, you can give me a follow on GitHub. Happy coding!