Introduction to Go: HTTP
HTTP is a fundamental part of Microservices and Web distributed systems
Go has a built in HTTP web server package. The package can be found here
We simply have to import the http
package:
import (
"net/http"
)
In part 1, we covered the fundamentals of writing basic Go
In part 2, we've learn how to use basic data structures like json
so we can send\receive data.
We'll be combining both these techniques so we can serve our videos
data over a web endpoint.
As always, let's start with our dockerfile
, main.go
and videos.go
we created in Part 2
Dev Environment
The same as Part 1+2, we start with a dockerfile where we declare our version of go
.
cd golang\introduction\part-3.http
docker build --target dev . -t go
docker run -it -v ${PWD}:/work go sh
go version
Create our App
Create a new directory that holds defines our repository
and holds our module
mkdir videos
- Define a module path
# change directory to your application source code
cd videos
# create a go module file
go mod init videos
Create our base code
We start out all our applications with a main.go
defining our package
, declaring our import
dependencies
and our entrypoint main()
function
package main
import "fmt"
func main() {
fmt.Println("Hello, world.")
}
Create our Videos app
Firstly, we create a seperate code file videos.go
that deals with our YouTube videos
The videos.go
file defines what a video struct
looks like, a getVideos()
function to retrieve
videos list as a slice and a saveVideos()
function to save videos to a file locally.
Let's copy the following content from Part 2 and create videos.go
:
We want videos.go
to be part of package main:
package main
We import 2 packages, 1 for reading and writing files, and another for dealing with json
import (
"io/ioutil"
"encoding/json"
)
Then we define what a video struct
looks like:
type video struct {
Id string
Title string
Description string
Imageurl string
Url string
}
We have a function for retrieving video
objects as a list of type slice
:
func getVideos()(videos []video){
fileBytes, err := ioutil.ReadFile("./videos.json")
if err != nil {
panic(err)
}
err = json.Unmarshal(fileBytes, &videos)
if err != nil {
panic(err)
}
return videos
}
We also need to copy our videos.json
file which contains our video data.
And finally, we have a function that accepts a list of type slice
and stores the videos to a local file
func saveVideos(videos []video)(){
videoBytes, err := json.Marshal(videos)
if err != nil {
panic(err)
}
err = ioutil.WriteFile("./videos-updated.json", videoBytes, 0644)
if err != nil {
panic(err)
}
}
HTTP Package
https://golang.org/pkg/net/http/
The HTTP package allows us to implement an HTTP client and a server. A client is a component that makes HTTP calls. A server is a component that receives or serves HTTP.
The HTTP package is capable of sending HTTP requests as well as defining a server for receiving HTTP requests.
We can use this to run an HTTP server to serve files, or serve data, like an API.
Let's define a server in main.go
:
# just one line :)
http.ListenAndServe(":8080", nil)
# ListenAndServe starts an HTTP server with a given address and handler.
# The handler is usually nil, which means to use DefaultServeMux.
# Handle and HandleFunc add handlers to DefaultServeMux
Now before we run this, since we're running in Docker, we want to exit the container
and rerun it, but this time open port 8080
docker run -it -p 8080:8080 -v ${PWD}:/work go sh
cd videos
go run main.go
# you will notice the application pausing
We should see our server with a 404 on http://localhost:8080/
Handle HTTP requests
In order to handle requests, we can tell the HTTP service that we want it to run a function
for the request coming in.
We can see the http
package has a HandleFunc
function: https://golang.org/pkg/net/http/
To see this in action, lets create a Hello()
function:
func Hello(){
}
And tell our http
service to run it:
http.HandleFunc("/", Hello)
We cannot run this yet. As per http
documentation, our Hello
function needs to take in some inputs.
func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
Therefore we need to add inputs to our function:
func Hello(w http.ResponseWriter, r *http.Request){
}
This allows us to get the request, its body
, headers
and a write where we can send a response.
Run this in the browser and you will notice the 404 goes away, but we now get an empty response.
HTTP Response
Let's write a reponse to the incoming request.
The response write has a Write()
function that takes a bunch of bytes.
We can convert string to bytes by casting a string
to a []byte
like:
[]byte("Hello!")
. Let's convert it and write "Hello" to the response:
w.Write([]byte("Hello!"))
IF we run this code, we can see "Hello!" in the response body
HTTP Headers
Headers play an important role in HTTP communication.
Lets access all the headers of the incoming request!
If we look at the Header definition here, we can see how to access it.
Let's use the for
loop we learnt in part 1
for i, value := range r.Header {
}
We learn't from our loop, we have in indexer and a value.
For i
, we can rename it to header since it represents the header key in the dictionary.
And the value
is the value of type []string
, containing the value of the header:
for header, value := range r.Header {
fmt.Printf("Key: %v \t Value: %v \n", header, value)
}
We can use fmt
to print out the values and look at the headers.
We can also set headers on our response.
If we take a look at the http
docs, we can see header is also a dictionary or strings.
w.Header().Add("TestHeader", "TestValue")
You can now see the headers in the response value if you use curl
or your browser development tools
HTTP Methods | GET
Web servers can serve data in a number of ways and support multiple type of HTTP methods.
GET
is used to request data from a specified resource.
So far, our HTTP route for our Hello function is using the GET
method.
Let's make our GET
method more useful by serving our video data
Let's rename our Hello()
function to HandleGetVideos()
.
Our /
route will return all videos:
videos := getVideos()
In part 2 we covered JSON
.
We need to convert our video slice
of struct
's to JSON
in order to return it to the client.
For this we learnt about the Marshall function:
Import the JSON
package:
"encoding/json"
Convert our videos to JSON
:
videoBytes, err := json.Marshal(videos)
if err != nil {
panic(err)
}
w.Write(videoBytes)
If we run this code and hit our /
endpoint, we can now see JSON
data being returned.
This is a core part of building an API in Go.
HTTP Methods | POST
A POST
method is used to send data to a server to create/update a resource.
Since we built a saveVideos
function, lets use that so a client can update videos!
We need to define a new route, we can all it /update
:
http.HandleFunc("/update", HandleUpdateVideos)
And we need to define an HandleUpdateVideos()
function:
func HandleUpdateVideos(w http.ResponseWriter, r *http.Request){
}
Let's validate the request method to ensure its POST
We need to also ensure we send a status code to inform the user of method not allowed.
https://golang.org/pkg/net/http/#ResponseWriter
if r.Method == "POST" {
//update our videos here!
} else {
w.WriteHeader(405)
fmt.Fprintf(w, "Method not Supported!")
}
Now we need to accept JSON
from the POST
request body
https://golang.org/pkg/net/http/#Request
In the docs above, we can see the request Body is of type Body io.ReadCloser
To read that, we can use the ioutil
package
https://golang.org/pkg/io/ioutil/
import "io/ioutil"
Then we can read the body into a slice
of bytes
:
body, err := ioutil.ReadAll(r.Body)
if err != nil {
panic(err)
}
Now that we have the body in a []byte
, we need to use our knowledge from part 2 where we
convert []byte
to a slice
of video
items.
var videos []video
err = json.Unmarshal(body, &videos)
if err != nil {
panic(err)
}
Creating our video objects allows us to do some validation if we wanted to.
We can ensure the request body adheres to our API contract for this videos API.
So instead of calling panic
, lets return a 400
Bad request status code if we cannot
Unmarshal the JSON
data. This might help with some basic validation.
w.WriteHeader(400)
fmt.Fprintf(w, "Bad request")
And Finally, let's update our videos file! :
saveVideos(videos)
Build our Docker container
Let's uncomment all the build lines in the dockerfile
Full dockerfile
:
FROM golang:1.15-alpine as dev
WORKDIR /work
FROM golang:1.15-alpine as build
WORKDIR /videos
COPY ./videos/* /videos/
RUN go build -o videos
FROM alpine as runtime
COPY --from=build /videos/videos /
COPY ./videos/videos.json /
CMD ./videos
Build :
cd golang\introduction\part-3.http
docker build . -t videos
Run :
docker run -it -p 8080:8080 videos
Things to know
- SSL for secure web connection
- Authentication
- Good API validation
- Support a backwards compatible contract (Inputs remain consistent)