How to Write an HTTP REST API Server in Go in Minutes
In this tutorial, see how to write an HTTP REST API server in Golang.
Join the DZone community and get the full member experience.
Join For FreeLearning a new language is not easy, but with concrete examples and step-by-step instructions, it's a powerful way to succeed at it. For this reason, I decided to write a series of step-by-step tutorials.
Let's use the power of Go to create an HTTP REST API server in Go.
You might also like: Benefits of REST APIs With HTTP/2
Go, Go, Go
Like the previous article, the first thing to do is to install GVM (a Go version manager) and, of course, Go.
In order to install Go, you can follow the installation procedure on the official website or you can install Go through GVM. GVM is a very practical version for Go, which allows you to update your version of Go by specifying which version you want.
Installation:
For bash:
bash < <(curl -s-S-L https://raw.githubusercontent.com/moovweb/gvm/master/binscripts/gvm-installer)
For zsh:
zsh < <(curl -s-S-L https://raw.githubusercontent.com/moovweb/gvm/master/binscripts/gvm-installer)
Usage:
$ gvm
Usage: gvm [command]
Description:
GVM is the Go Version Manager
Commands:
version — print the gvm version number
get — gets the latest code (for debugging)
use — select a go version to use (--default to set permanently)
diff — view changes to Go root
help — display this usage text
implode — completely remove gvm
install — install go versions
uninstall — uninstall go versions
cross — install go cross compilers
linkthis — link this directory into GOPATH
list — list installed go versions
listall — list available versions
alias — manage go version aliases
pkgset — manage go packages sets
pkgenv — edit the environment for a package set
The GVM command that will interest us especially is the command gvm install, which is used like this:
$ gvm install [version] [options]
Go installation:
$ gvm install go1.13.3 -B
$ gvm use go1.13.3 --default
In your .zshrc or .bashrc file, set your $GOROOT and $GOPATH environment variables. Here is an example:
[[ -s"$HOME/.gvm/scripts/gvm" ]] && source"$HOME/.gvm/scripts/gvm"
exportGOPATH=$HOME/go
exportGOBIN=$GOPATH/bin
exportPATH=${PATH}:$GOBIN
That's it, Go is installed along with its version manager. Now it's time to get to the heart of the matter and create our first CLI.
Init Your App
Now it's time to create our repository in GitHub (in order to share and open-source it).
For that, I logged in Github website, clicked on the repositories link, and created a new repository called “http-go-server”:
Now, in your local machines, git clone this new repo where you want.
"Where I want? Are you sure?"
We will use Go modules for dependencies so don't forget to tell us to git clone outside of our GOPATH. In fact, good news, since Go version 1.13, we no longer have to worry about it. Go modules now works outside and inside the GOPATH!
It's the reason I have chosen to use Go version 1.13 in this article.
So, we have to git clone the repo in order to retrieve it in our machine and to sync it to the git repository:
$ git clone https://github.com/scraly/http-go-server.git
$ cd http-go-server
Now, we have to initialize go modules (dependencies management):
$ go mod init github.com/scraly/http-go-server
go: creating new go.mod: module github.com.scraly/http-go-server
We will create a simple HTTP server but with good practices in code organization. So we will not put all our code in a main.go file and that's it, but we will create a code organization and organize our code.
Create the following folders organization:
.
├── README.md
├── bin
├── doc
├── go.mod
├── internal
├── pkg
│ └── swagger
└── scripts
Let's Create an HTTP Server
Now it's time to code our HTTP server.
Go is a powerful language. One of its powers is to come to a lot of built-in packages like net/HTTP that will interest us.
We will start to create a main.go file in the internal/ folder like this:
package main
import (
"fmt"
"html"
"log"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})
log.Println(“Listening on localhost:8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
This simple example starts an HTTP server, listens on port 8080 incoming requests, and serves on /.
Now, it’s time to build our app in order to test it and then launch the app binary:
$ go run internal/main.go
2019/10/27 20:53:39 Listening on localhost:8080 ...
In order to test your HTTP server, you can curl on localhost:8080 or go to this endpoint in your browser:
$ curl localhost:8080
Hello, "/"%
Great, we created a tiny HTTP server and it’s working.
And now we can build it in a binary executable fine:
$ go build -o bin/http-go-server internal/main.go
Perfect, we’ve done it in minutes...yes but we will go further and further in this article :-).
Let’s Use a Makefile
I don’t want to execute each of the commands manually, so, for the continuous improvement of our apps, a good practice is to create a Makefile file and then we could build our app, generate files, generate swagger docs, check licenses, and execute unit tests and static tests directly through making commands.
We can define a set of tasks in a Makefile (a make file) that will be executed by make tool.
So, for this project, I created a Makefile that you can directly download, and it will be time-saving.
Makefile: https://raw.githubusercontent.com/scraly/http-go-server/master/Makefile
For the curious, in a Makefile, we create several targets that will execute one or several commands.
To summarize, a target needs, or not, prerequisites and will execute a recipe:
target: prerequisites
<TAB> recipe
In my Makefile I created, for example, a build target that builds and packages our app in a bin/http-go-server binary file:
## Build all binaries
build:
$(GO) build -o bin/http-go-server internal/main.go
HTTP Endpoints Definition Time
We will now step up our HTTP server and use Swagger, which handles definitions of our HTTP endpoints.
What Is Swagger?
Swagger allows you to provide standardized documentation of your APIs compliant to OpenAPI specifications.
With a swagger specification file in input, thanks to the Swagger application, you can generate the code and at the end, and you can provide users the API documentation in HTML.
If you want to build a public API, don't hesitate to use Swagger.
Swagger installation:
Please refer to go-swagger installation page.
And then, you can check the version of Swagger app in order to check the tool is correctly installed in your system.
$ swagger version
The first things to do now is to create our swagger specification file in our code:
pkg/swagger/swagger.yml:
consumes:
- application/json
info:
description: HTTP server in Go with Swagger endpoints definition
title: http-go-server
version: 0.1.0
produces:
- application/json
schemes:
- http
swagger: "2.0"
paths:
/healthz:
get:
operationId: checkHealth
produces:
- text/plain
responses:
'200':
description: OK message
schema:
type: string
enum:
- OK
/hello/{user}:
get:
description: Returns a greeting to the user!
parameters:
- name: user
in: path
type: string
required: true
description: The name of the user to greet.
responses:
200:
description: Returns the greeting.
schema:
type: string
400:
description: Invalid characters in "user" were provided.
After each modification of a swagger file, a good practice is to check the validity of the file.
In order to do that, we can use the command swagger validate:
$ swagger validate pkg/swagger/swagger.yml
2019/10/27 21:14:47
The swagger spec at "pkg/swagger/swagger.yml" is valid against swagger specification 2.0
Or we can use a Makefile target:
$ make swagger.validate
2019/10/27 21:15:12
The swagger spec at "pkg/swagger/swagger.yml" is valid against swagger specification 2.0
Cool, our swagger file is valid.
We will now create our swagger definitions in an HTML doc. For that, I use a docker image, which takes into consideration our swagger YAML definition and returns a pretty HTML page:
$ make swagger.doc
If you open the generated doc/index.html page in a browser, you can view HTML endpoints definitions:
Nice, it’s human-readable.
Now, we can generate Go code thanks to swagger specifications.
In order to do this, we go in the package pkg/swagger/ and now we will create a gen.go file like this:
package swagger
//go:generate rm -rf server
//go:generate mkdir -p server
//go:generate swagger generate server --quiet --target server --name hello-api --spec swagger.yml --exclude-main
Thanks to our Makefile we can execute make generate for generating swagger go code:
$ make generate
==> generating go code
GOFLAGS=-mod=vendor go generate github.com.scraly/http-go-server/internal github.com.scraly/http-go-server/pkg/swagger
As you can see below, with a swagger endpoint definitions in input, a lot of code has been generated, and it’s time-saving for our HTTP server implementation.
Let’s edit our main.go file with the use of Swagger:
package main
import (
"log"
"github.com/go-openapi/loads"
"github.com/scraly/http-go-server/pkg/swagger/server/restapi"
"github.com/scraly/http-go-server/pkg/swagger/server/restapi/operations"
)
func main() {
// Initialize Swagger
swaggerSpec, err := loads.Analyzed(restapi.SwaggerJSON, "")
if err != nil {
log.Fatalln(err)
}
api := operations.NewHelloAPI(swaggerSpec)
server := restapi.NewServer(api)
defer server.Shutdown()
server.Port = 8080
// Start listening using having the handlers and port
// already set up.
if err := server.Serve(); err != nil {
log.Fatalln(err)
}
}
Let’s launch our server:
$ go run internal/main.go
2019/10/28 14:27:26 Serving hello at http://[::]:8080
And now we can do several tests:
$ curl localhost:8080
{"code":404,"message":"path / was not found"}%
$ curl localhost:8080/hello
{"code":404,"message":"path /hello was not found"}%
$ curl localhost:8080/hello/aurelie
"operation GetHelloUser has not yet been implemented"
Perfect, our HTTP server is answering and even tells us that GetHelloUser has not yet been implemented, so let’s implement it!
Edit main.go file like this:
package main
import (
"log"
"github.com/go-openapi/loads"
"github.com/go-openapi/runtime/middleware"
"github.com/scraly/http-go-server/pkg/swagger/server/restapi"
"github.com/scraly/http-go-server/pkg/swagger/server/restapi/operations"
)
func main() {
// Initialize Swagger
swaggerSpec, err := loads.Analyzed(restapi.SwaggerJSON, "")
if err != nil {
log.Fatalln(err)
}
api := operations.NewHelloAPI(swaggerSpec)
server := restapi.NewServer(api)
defer func() {
if err := server.Shutdown(); err != nil {
// error handle
log.Fatalln(err)
}
}()
server.Port = 8080
// Implement the CheckHealth handler
api.CheckHealthHandler = operations.CheckHealthHandlerFunc(
func(user operations.CheckHealthParams) middleware.Responder {
return operations.NewCheckHealthOK().WithPayload("OK")
})
// Implement the GetHelloUser handler
api.GetHelloUserHandler = operations.GetHelloUserHandlerFunc(
func(user operations.GetHelloUserParams) middleware.Responder {
return operations.NewGetHelloUserOK().WithPayload("Hello " + user.User + "!")
})
// Start server which listening
if err := server.Serve(); err != nil {
log.Fatalln(err)
}
}
Once again, we e-launch our server:
$ go run internal/main.go
2019/10/28 21:45:38 Serving hello at http://[::]:8080
$ curl localhost:8080/hello/aurelie
"Hello aurelie!"
$ curl localhost:8080/healthz
OK%
Great, we have an HTTP server following OpenAPI specifications and two routes:
- GET /healthz
- GET /hello/{name}
We can stop here because our HTTP server is working, but we will go further.
We will create new functions for routes implementation in our main.go file (at the end of the file):
//Health route returns OK
func Health(operations.CheckHealthParams) middleware.Responder {
return operations.NewCheckHealthOK().WithPayload("OK")
}
//GetHelloUser returns Hello + your name
func GetHelloUser(user operations.GetHelloUserParams) middleware.Responder {
return operations.NewGetHelloUserOK().WithPayload("Hello " + user.User + "!")
}
And now we just need to call theses new functions in the main function:
func main() {
// Initialize Swagger
swaggerSpec, err := loads.Analyzed(restapi.SwaggerJSON, "")
if err != nil {
log.Fatalln(err)
}
api := operations.NewHelloAPI(swaggerSpec)
server := restapi.NewServer(api)
defer func() {
if err := server.Shutdown(); err != nil {
// error handle
log.Fatalln(err)
}
}()
server.Port = 8080
api.CheckHealthHandler = operations.CheckHealthHandlerFunc(Health)
api.GetHelloUserHandler = operations.GetHelloUserHandlerFunc(GetHelloUser)
// Start server which listening
if err := server.Serve(); err != nil {
log.Fatalln(err)
}
}
Like usual, we test if static tests pass and if our app builds:
$ make lint.full
==> linters (slow)
INFO [config_reader] Config search paths: [./ /Users/uidn3817/git/github.com/scraly/http-go-server/internal /Users/uidn3817/git/github.com/scraly/http-go-server /Users/uidn3817/git/github.com/scraly /Users/uidn3817/git/github.com /Users/uidn3817/git /Users/uidn3817 /Users /]
INFO [config_reader] Used config file .golangci.yml
INFO [lintersdb] Active 13 linters: [deadcode errcheck goimports golint govet ineffassign maligned misspell nakedret structcheck typecheck unconvert varcheck]
INFO [loader] Go packages loading at mode load types and syntax took 1.474090863s
INFO [loader] SSA repr building timing: packages building 15.964643ms, total 220.705097ms
INFO [runner] worker.4 took 652.824µs with stages: deadcode: 244.82µs, unconvert: 110.42µs, errcheck: 102.565µs, varcheck: 81.099µs, structcheck: 38.623µs, maligned: 34.263µs, nakedret: 22.825µs, typecheck: 5.339µs
INFO [runner] worker.6 took 1.883µs
INFO [runner] worker.8 took 2.125µs
INFO [runner] worker.5 took 1.040528ms with stages: ineffassign: 1.035173ms
INFO [runner] worker.7 took 3.211184ms with stages: goimports: 3.2029ms
INFO [runner] worker.3 took 102.06494ms with stages: misspell: 102.056568ms
INFO [runner] worker.1 took 120.104406ms with stages: golint: 120.096599ms
INFO [runner] worker.2 took 204.48169ms with stages: govet: 204.471908ms
INFO [runner] Workers idle times: #1: 84.514968ms, #3: 86.547645ms, #4: 203.167851ms, #5: 202.957443ms, #6: 203.09743ms, #7: 201.160969ms, #8: 202.973757ms
INFO [runner] processing took 18.697µs with stages: max_same_issues: 14.808µs, skip_dirs: 737ns, cgo: 498ns, nolint: 420ns, filename_unadjuster: 398ns, max_from_linter: 281ns, autogenerated_exclude: 172ns, path_prettifier: 170ns, identifier_marker: 167ns, diff: 164ns, skip_files: 161ns, replacement_builder: 158ns, exclude: 156ns, source_code: 90ns, max_per_file_from_linter: 81ns, path_shortener: 79ns, uniq_by_line: 79ns, exclude-rules: 78ns
INFO File cache stats: 0 entries of total size 0B
INFO Memory: 24 samples, avg is 248.1MB, max is 671.8MB
INFO Execution took 2.277787079s
Test Static Code
Another good practice is to test static code with linters analysis. For that, we use golang-ci tool (a fast linter in Go, better than gometalinter).
Thanks to our Makefile, you just need to get the tools I listed here, like golang-ci:
$ make get.tools
A good practice is to set-up a .golangci.yml file in order to define linter properties we want. Here is an example of golang-ci properties:
run:
modules-download-mode: vendor
deadline: 10m
issues-exit-code: 1
tests: true
skip-files:
- ".*\\.pb\\.go$"
- ".*\\.gen\\.go$"
- "mock_.*\\.go"
linters:
enable:
- govet # check standard vet rules
- golint # check standard linting rules
- staticcheck# comprehensive checks
- errcheck # find unchecked errors
- ineffassign# find ineffective assignments
- varcheck # find unused global variables and constants
- structcheck# check for unused struct parameters
- deadcode # find code that is not used
- nakedret # check for naked returns
- goimports # fix import order
- misspell # check spelling
- unconvert # remove unnecessary conversions
- maligned # check for better memory usage
disable:
- goconst # check for things that could be replaced by constants
- gocyclo # needs tweaking
- depguard # unused
- gosec # needs tweaking
- dupl # slow
- interfacer # not that useful
- gosimple # part of staticcheck
- unused # part of staticcheck
- megacheck # part of staticcheck
- lll
fast: false
output:
format: colored-line-number
print-issued-lines: true
print-linter-name: true
linters-settings:
errcheck:
# report about not checking of errors in type assetions: `a := b.(MyStruct)`;
# default is false: such cases aren't reported by default.
check-type-assertions: false
# report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`;
# default is false: such cases aren't reported by default.
check-blank: false
govet:
# report about shadowed variables
#TODO# check-shadowing: true
# Obtain type information from installed (to $GOPATH/pkg) package files:
# golangci-lint will execute `go install -i` and `go test -i` for analyzed packages
# before analyzing them.
# Enable this option only if all conditions are met:
# 1. you use only "fast" linters (--fast e.g.): no program loading occurs
# 2. you use go >= 1.10
# 3. you do repeated runs (false for CI) or cache $GOPATH/pkg or `go env GOCACHE` dir in CI.
use-installed-packages: false
golint:
min-confidence: 0.8
gofmt:
simplify: true
gocyclo:
min-complexity: 10
maligned:
suggest-new: true
dupl:
threshold: 150
goconst:
min-len: 3
min-occurrences: 3
misspell:
locale: US
lll:
line-length: 140
tab-width: 1
unused:
# treat code as a program (not a library) and report unused exported identifiers; default is false.
# XXX: if you enable this setting, unused will report a lot of false-positives in text editors:
# if it's called for subdir of a project it can't find funcs usages. All text editor integrations
# with golangci-lint call it on a directory with the changed file.
check-exported: false
unparam:
# call graph construction algorithm (cha, rta). In general, use cha for libraries,
# and rta for programs with main packages. Default is cha.
algo: cha
# Inspect exported functions, default is false. Set to true if no external program/library imports your code.
# XXX: if you enable this setting, unparam will report a lot of false-positives in text editors:
# if it's called for subdir of a project it can't find external interfaces. All text editor integrations
# with golangci-lint call it on a directory with the changed file.
check-exported: false
nakedret:
# make an issue if func has more lines of code than this setting and it has naked returns; default is 30
max-func-lines: 30
prealloc:
# Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them.
# True by default.
simple: true
range-loops: true# Report preallocation suggestions on range loops, true by default
for-loops: false# Report preallocation suggestions on for loops, false by default
issues:
max-per-linter: 0
max-same: 0
new: false
exclude-use-default: false
Now we can check if our code contains lint errors or not:
$ make lint.full
==> linters (slow)
INFO [config_reader] Config search paths: [./ /Users/uidn3817/git/github.com/scraly/http-go-server/internal /Users/uidn3817/git/github.com/scraly/http-go-server /Users/uidn3817/git/github.com/scraly /Users/uidn3817/git/github.com /Users/uidn3817/git /Users/uidn3817 /Users /]
INFO [config_reader] Used config file .golangci.yml
INFO [lintersdb] Active 13 linters: [deadcode errcheck goimports golint govet ineffassign maligned misspell nakedret structcheck typecheck unconvert varcheck]
INFO [loader] Go packages loading at mode load types and syntax took 1.403040989s
INFO [loader] SSA repr building timing: packages building 17.446103ms, total 215.11635ms
INFO [runner] worker.1 took 319.338µs with stages: unconvert: 126.709µs, structcheck: 105.706µs, varcheck: 80.624µs
INFO [runner] worker.8 took 279.76µs with stages: errcheck: 102.203µs, nakedret: 88.6µs, deadcode: 54.547µs, maligned: 22.796µs, typecheck: 2.416µs
INFO [runner] worker.2 took 908ns
INFO [runner] worker.7 took 1.424891ms with stages: ineffassign: 1.419068ms
INFO [runner] worker.4 took 2.395856ms with stages: goimports: 2.39105ms
INFO [runner] worker.6 took 75.843872ms with stages: golint: 75.832987ms
INFO [runner] worker.5 took 77.126536ms with stages: misspell: 77.092913ms
INFO [runner] worker.3 took 124.506172ms with stages: govet: 124.498137ms
INFO [runner] Workers idle times: #1: 124.053298ms, #2: 123.973576ms, #4: 122.078696ms, #5: 47.339761ms, #6: 48.693713ms, #7: 122.946009ms, #8: 124.035904ms
INFO [runner] processing took 19.597µs with stages: max_same_issues: 16.123µs, cgo: 541ns, skip_dirs: 493ns, nolint: 398ns, max_from_linter: 280ns, path_prettifier: 179ns, filename_unadjuster: 172ns, replacement_builder: 170ns, autogenerated_exclude: 170ns, exclude: 164ns, diff: 162ns, skip_files: 161ns, identifier_marker: 150ns, source_code: 97ns, path_shortener: 97ns, max_per_file_from_linter: 82ns, exclude-rules: 80ns, uniq_by_line: 78ns
INFO File cache stats: 0 entries of total size 0B
INFO Memory: 23 samples, avg is 255.8MB, max is 672.0MB
INFO Execution took 2.119246564s
Good, everything is fine.
If you want to edit the .golangci.yml file, please take a look to golang-ci supported linters.
Check Licenses
Another good practice to see it’s the license checking.
You need to check licenses (licenses used by your dependencies), for example, when you want to open-source your application or in your company in order to not use forbidden license.
In Go, a tool exists called wwhrd.
First of all, we will create a file named .wwhrd.yml in order to define options:
---
blacklist:
- AGPL-3.0
- GPL-2.0
- GPL-3.0
- CDDL-1.0
whitelist:
- Apache-2.0
- MIT
- NewBSD
- FreeBSD
- LGPL-2.1
- LGPL-3.0
- ISC
- MPL-2.0
- EPL-1.0
- Unlicense
# exceptions:
# - github.com/davecgh/go-spew/spew/... # ISC License misrecognized
# - github.com/dchest/uniuri # https://creativecommons.org/publicdomain/zero/1.0/
In this properties file for wwhrd, you can add exceptions, blacklists, and whitelists repositories.
A target for licenses checking exists in the Makefile so you just need to execute it:
$ make license
==> license check
wwhrd check
INFO[0000] Found Approved license license=Apache-2.0 package=go.mongodb.org/mongo-driver/bson/primitive
INFO[0000] Found Approved license license=Apache-2.0 package=github.com/go-openapi/swag
INFO[0000] Found Approved license license=NewBSD package=github.com/PuerkitoBio/purell
INFO[0000] Found Approved license license=Apache-2.0 package=github.com/go-openapi/jsonreference
INFO[0000] Found Approved license license=Apache-2.0 package=go.mongodb.org/mongo-driver/bson/bsoncodec
INFO[0000] Found Approved license license=Apache-2.0 package=github.com/go-openapi/loads
…
Great, no license problems.
Build Our App
And now we can build our app in an executable binary file and test the binary.
$ make build
go build -o bin/http-go-server internal/main.go
$ ./bin/http-go-server
2019/10/28 21:47:38 Serving hello at http://[::]:8080
Cool :-)
Conclusion
As you saw in the first part of this article, it’s possible to create an HTTP server in minutes or seconds, with net/http package and Gorilla/mux for router, but I wanted to show you, step-by-step, how to go further with best practices in the code organization. The use of Swagger in order to be compliant with OpenAPI standard and the use of several useful tools.
At the end, our little HTTP server app grew up a little bit, as you can see in the code organization:
.
├── Makefile
├── README.md
├── bin
│ └── http-go-server
├── doc
│ └── index.html
├── go.mod
├── go.sum
├── internal
│ └── main.go
├── pkg
│ └── swagger
│ ├── gen.go
│ ├── server
│ │ └── restapi
│ │ ├── configure_hello.go
│ │ ├── doc.go
│ │ ├── embedded_spec.go
│ │ ├── operations
│ │ │ ├── check_health.go
│ │ │ ├── check_health_parameters.go
│ │ │ ├── check_health_responses.go
│ │ │ ├── check_health_urlbuilder.go
│ │ │ ├── get_hello_user.go
│ │ │ ├── get_hello_user_parameters.go
│ │ │ ├── get_hello_user_responses.go
│ │ │ ├── get_hello_user_urlbuilder.go
│ │ │ └── hello_api.go
│ │ └── server.go
│ └── swagger.yml
├── scripts
└── vendor
├──…
└── modules.txt
└── .gitignore
└── .golangci.yml
└── .wwhrd.yml
All the code is available in the Github repository: https://github.com/scraly/http-go-server
I hope this kind of article will help you, and if it did, I will create more and more articles in order to discover the Go ecosystem.
Further Reading
Create and Publish Your Rest API Using Spring Boot and Heroku
Opinions expressed by DZone contributors are their own.
Comments