HTTP basics With Go 1.22
Rate this tutorial
Go is a wonderful programming language –very productive with many capabilities. This series of articles is designed to offer you a feature walkthrough of the language, while we build a realistic program from scratch.
In order for this to work, there are some things we must agree upon:
- This is not a comprehensive explanation of the Go syntax. I will only explain the bits strictly needed to write the code of these articles.
- Typing the code is better than just copying and pasting, but do as you wish.
- Materials are available to try by yourself at your own pace, but it is recommended to play along if you do this in a live session.
- If you are a Golang newbie, type and believe. If you have written some Go, ask any questions. If you have Golang experience, there are comments about best practices –let's discuss those. In summary: Ask about the syntax, ask about the magic, talk about the advanced topics, or go to the bar.
- Finally, although we are only going to cover the essential parts, the product of this series is the seed for a note-keeping back end where we will deal with the notes and its metadata. I hope you like it.
- Let's start by creating a directory for our project and initializing the project. Create a directory and get into it. Initialize the project as a Go module with the identifier “github.com/jdortiz/go-intro,” which you should change to something that is unique and owned by you.
1 go mod init github.com/jdortiz/go-intro - In the file explorer of VSCode, add a new file called
main.go
with the following content:1 package main 2 3 import "fmt" 4 5 func main() { 6 fmt.Println("Hola Caracola") 7 } - Let's go together through the contents of that file to understand what we are doing here.
- Every source file must belong to a
package
. All the files in a directory must belong to the same package. Packagemain
is where you should create yourmain
function. func
is the keyword for declaring functions andmain
is where your program starts to run at.fmt.Println()
is a function of the standard library (stdlib) to print some text to the standard output. It belongs to thefmt
package.- Having the
import
statement allows us to use thefmt
package in the code, as we are doing with thefmt.Println()
function.
- The environment is configured so we can run the program from VS Code. Use "Run and Debug" on the left bar and execute the program. The message "Hola caracola" will show up on the debug console.
- You can also run the program from the embedded terminal by using
1 go run main.go
- Go's standard library includes all the pieces needed to create a full-fledged HTTP server. Until version 1.22, using third-party packages for additional functionality, such as easily routing requests based on the HTTP verb, was very common. Go 1.22 has added most of the features of those packages in a backward compatible way.
- Webservers listen to requests done to a given IP address and port. Let's define that in a constant inside of the main function:
1 const serverAddr string = "127.0.0.1:8081" - If we want to reply to requests sent to the root directory of our web server, we must tell it that we are interested in that URL path and what we want to happen when a request is received. We do this by using
http.HandleFunc()
at the bottom of the main function, with two parameters: a pattern and a function. The pattern indicates the path that we are interested in (like in"/"
or"/customers"
) but, since Go 1.22, the pattern can also be used to specify the HTTP verb, restrict to a given host name, and/or extract parameters from the URL. We will use"GET /"
, meaning that we are interested in GET requests to the root. The function takes two parameters: anhttp.ResponseWriter
, used to produce the response, and anhttp.Request
that holds the request data. We will be using an anonymous function (a.k.a. lambda) that initially doesn't do anything. You will need to import the "net/http" package, and VS Code can do it automatically using its quick fix features.1 http.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) { 2 }) - Inside of our lambda, we can use the response writer to add a message to our response. We use the
Write()
method of the response writer that takes a slice of bytes (i.e., a "view" of an array), so we need to convert the string. HTML could be added here.1 w.Write([]byte("HTTP Caracola")) - Tell the server to accept connections to the IP address and port with the functionality that we have just set up. Do it after the whole invocation to
http.HandleFunc()
.1 http.ListenAndServe(serverAddr, nil) http.ListenAndServe()
returns an error when it finishes. It is a good idea to wrap it with another function that will log the message when that happens.log
also needs to be imported: Do it yourself if VSCode didn't take care of it.1 log.Fatal(http.ListenAndServe(serverAddr, nil)) - Compile and run. The codespace will offer to use a browser or open the port. You can ignore this for now.
- If you run the program from the terminal, open a second terminal using the ".
1 curl -i localhost:8081/
- HTTP handlers can also be implemented as regular functions –i.e., non-anonymous– and are actually easier to maintain. Let's define one for an endpoint that can be used to create a note after the
main
function.1 func createNote(w http.ResponseWriter, r *http.Request) { 2 } - Before we can implement that handler, we need to define a type that will hold the data for a note. The simplest note could have a title and text. We will put this code before the
main
function.1 type Note struct { 2 Title string 3 Text string 4 } - But we can have some more data, like a list of categories, that in Go is represented as a slice of strings (
[]string
), or a field that uses another type that defines the scope of this note as a combination of a project and an area. The complete definition of these types would be:1 type Scope struct { 2 Project string 3 Area string 4 } 5 6 type Note struct { 7 Title string 8 Tags []string 9 Text string 10 Scope Scope 11 } - Notice that both the names of the types and the names of the fields start with a capital letter. That is the way to say in Go that something is exported and it would also apply to function names. It is similar to using a
public
attribute in other programming languages. - Also, notice that field declarations have the name of the field first and its type later. The latest field is called "Scope," because it is exported, and its type, defined a few lines above, is also called Scope. No problem here –Go will understand the difference based on the position.
- Inside of our
createNote()
handler, we can now define a variable for that type. The order is also variable name first, type second.note
is a valid variable from here on, but at the moment all the fields are empty.1 var note Note - Data is exchanged between HTTP servers and clients using some serialization format. One of the most common ones nowadays is JSON. After the previous line, let's create a decoder that can convert bytes from the HTTP request stream into an actual object. The
encoding/json
package of the standard library provides what we need. Notice that I hadn't declared thedecoder
variable. I use the "short variable declaration" (:=
), which declares and assigns value to the variable. In this case, Go is also doing type inference.1 decoder := json.NewDecoder(r.Body) - This decoder can now be used in the next line to deserialize the data in the HTTP request. That method returns an error, which will be
nil
(no value) if everything went well, or some (error) value otherwise. Notice that we use&
to pass a reference to the variable, so the method can change its value.1 err := decoder.Decode(¬e) - The expression can be wrapped to be used as the condition in an if statement. It is perfectly fine in Go to obtain some value and then compare in an expression after a semicolon. There are no parentheses surrounding the conditional expression.
1 if err := decoder.Decode(¬e); err != nil { 2 } - If anything goes wrong, we want to inform the HTTP client that there is a problem and exit the function. This early exit is very common when you handle errors in Go.
http.Error()
is provided by thenet/http
package, writes to the response writer the provided error message, and sets the HTTP status.1 http.Error(w, err.Error(), http.StatusBadRequest) 2 return - If all goes well, we just print the value of the note that was sent by the client. Here, we use another function of the
fmt
package that writes to a Writer the given data, using a format string. Format strings are similar to the ones used in C but with some extra options and more safety."%+v"
means print the value in a default format and include the field names (% to denote this is a format specifier, v for printing the value, the + for including the field names).1 fmt.Fprintf(w, "Note: %+v", note) - Let's add this handler to our server. It will be used when a POST request is sent to the
/notes
path.1 http.HandleFunc("POST /notes", createNote) - Run this new version.
- Let's first test what happens when it cannot deserialize the data. We should get a 400 status code and the error message in the body.
1 curl -iX POST localhost:8081/notes - Finally, let's see what happens when we pass some good data. The deserialized data will be printed to the standard output of the program.
1 curl -iX POST -d '{ "title": "Master plan", "tags": ["ai","users"], "text": "ubiquitous AI", "scope": {"project": "world domination", "area":"strategy"} }' localhost:8081/notes
In this article, we have learned:
- How to start and initialize a Go project.
- How to write a basic HTTP server from scratch using just Go standard library functionality.
- How to add endpoints to our HTTP server that provide different requests for different HTTP verbs in the client request.
- How to deserialize JSON data from the request and use it in our program.
Developing this kind of program in Go is quite easy and requires no external packages or, at least, not many. If this has been your first step into the world of Go programming, I hope that you have enjoyed it and that if you had some prior experience with Go, there was something of value for you.
In the next article of this series, we will go a step further and persist the data that we have exchanged with the HTTP client. This repository with all the code for this article and the next ones so you can follow along.
Stay curious. Hack your code. See you next time!
Top Comments in Forums
There are no comments on this article yet.
This is part of a series