After recently joining Central Tech, I was given my first project: implement a new product creation and querying API in Go. I took this as a chance for me to finally dive deep into Go, something I've been wanted to do after having heard praises about it from many others. I also decided to start detailing the things I learned along the way, both for personal reference and to ensure that I understand concepts enough to explain it in writing.
As such, this article and my site will hopefully come to act as a resource that I can refer back to, and hopefully be helpful to someone else as well.
This piece assumes that you have go installed and working on your computer. If not, please see the Go language's official getting started guide.
Before writing the actual functions to execute, we first need to define our program and import the necessary packages. Create a new file called
main.go
and add the following lines.package main
import (
"log"
"net/http"
)
Let's go over each of the components of the code above.
Packages are Go's source code, organized into system directories. They allow for code reusability across different applications. In order to differentiate an executable program from a library to be used by other programs, Go dictates that we includes
package main
in the header of the main file of any executables. When compiling, that package main
tells the Go compiler that the package should compile as an executable program rather than being built as a library.The keyword
import
is used for importing packages into our program or another package. When importing packages, the Go compiler will look in the locations specified by the two environment variables:$GOROOT
where packages from the standard library shipped with Go are stored$GOPATH
for fetched third-party or self-made packages.Note: Go by design does not allow declaration of unused import and will adamantly complain about it during compile time if any is found
In our case, both packages comes from the standard library:
log
allows us to log errors and other issues.net/http
gives us HTTP client and server implementation for building the actual APIAfter declaring the packages, we can begin defining and serving our API. Without using third party libraries and routers, there are two main 'approaches' to this.
If we implement our handler as a function, we'd use
http.HandleFunc
. Otherwise, if we'd implemented our handler as a type with a ServeHTTP
method, we'd use http.Handle
.For simple implementations, the first option is much easier and clearer to read and understand. But using a struct type allows storing of useful information in it, an example of which is the file server from the standard library. The struct contains the root directory for file service. See this Stack Overflow post for a more detailed explanation.
As our program is rather simple and straightforward, we'll stick to the first approach.
Note: HTTP handlers are processes that runs in response to a request made to a web application. For more information, see its Wikipedia page
Now we get to defining how our API server will actually behave, and the content we're going to send as our response. Add the following lines below our imports and package declarations:
func apiResponse(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"message":"hello world!"}`))
}
func main() {
http.HandleFunc("/",apiResponse)
log.Fatal(http.ListenAndServe(":8080",nil))
}
Again, let's run through each function.
The function apiResponse is responsible for writing the response code and JSON. The function itself takes two arguments:
w
of type http.ResponseWriter
assembles our HTTP server's responses, while r
reads and parse the requests. Now to go through each line of the function:-
w.WriteHeader(http.StatusOK)
writes the HTTP response code 200, indicating that the requested resource (the JSON response in our case) has been successfully fetched and is transmitted in the message body.-
w.Header().Set("Content-Type", "application/json")
indicates the return type of the requested resource.-
w.Write([]byte('{"message":"hello world!"}'))
finally writes our response in the message body.The
main()
function serves a special purpose in Go: it acts as the entry point of the executable programs. It does not take any argument nor return anything. And unlike some other languages, we do not need to explicitly call main
, as Go automatically calls the function when run.In our main function, the
HandleFunc
call tells the HTTP package to handle all requests to the web root ("/") with our apiResponse function. It then calls http.ListenAndServe
, specifying that it should listen on port 8080
on any interface.If we now run the command
go run .
and navigate to localhost:8080
on our web browser, we should see something like this.We can also enter the same URL into our REST Client (i.e. Postman, Insomnia, Paw) using the "GET" method to get the same result.
Now that our server can return a response, we can configure it to do different things depending on the method being requested.
To do so, we can modify our
apiResponse
function as such:func apiResponse(w http.ResponseWriter, r *http.Request) {
// Set the return Content-Type as JSON like before
w.Header().Set("Content-Type", "application/json")
// Change the response depending on the method being requested
switch r.Method {
case "GET":
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"message": "GET method requested"}`))
case "POST":
w.WriteHeader(http.StatusCreated)
w.Write([]byte(`{"message": "POST method requested"}`))
default:
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(`{"message": "Can't find method requested"}`))
}
}
Now if we close the previous version of our server with
ctrl-c
and start it once again, we should get different responses depending on the method we requested from our REST Client.Our final
main.go
should look something like:package main
import (
"log"
"net/http"
)
func apiResponse(w http.ResponseWriter, r *http.Request) {
// Set the return Content-Type as JSON like before
w.Header().Set("Content-Type", "application/json")
// Change the response depending on the method being requested
switch r.Method {
case "GET":
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"message": "GET method requested"}`))
case "POST":
w.WriteHeader(http.StatusCreated)
w.Write([]byte(`{"message": "POST method requested"}`))
default:
w.WriteHeader(http.StatusNotFound)a
w.Write([]byte(`{"message": "Can't find method requested"}`))
}
}
func main() {
http.HandleFunc("/",apiResponse)
log.Fatal(http.ListenAndServe(":8080",nil))
}
This covers the basic of building a REST API server using Go. In later posts I plan on going over the
net/http
library in more detail, as well as using third-party router libraries such as gorillia/mux and go-chi/chi for more complex routings.If you have any questions, feel free to reach out to me on Twitter or via email.