Explore Developer Center's New Chatbot! MongoDB AI Chatbot can be accessed at the top of your navigation to answer all your MongoDB questions.

Join us at AWS re:Invent 2024! Learn how to use MongoDB for AI use cases.
MongoDB Developer
Go
plus
Sign in to follow topics
MongoDB Developer Centerchevron-right
Developer Topicschevron-right
Languageschevron-right
Gochevron-right

HTTP basics With Go 1.22

Jorge D. Ortiz-Fuentes7 min read • Published Apr 17, 2024 • Updated Apr 23, 2024
Go
FULL APPLICATION
Facebook Icontwitter iconlinkedin icon
Rate this tutorial
star-empty
star-empty
star-empty
star-empty
star-empty

HTTP basics with Go 1.22

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.

Hello world

  1. 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.
    1go mod init github.com/jdortiz/go-intro
  2. In the file explorer of VSCode, add a new file called main.go with the following content:
    1package main
    2
    3import "fmt"
    4
    5func main() {
    6 fmt.Println("Hola Caracola")
    7}
  3. Let's go together through the contents of that file to understand what we are doing here.
    1. Every source file must belong to a package. All the files in a directory must belong to the same package. Package main is where you should create your main function.
    2. func is the keyword for declaring functions and main is where your program starts to run at.
    3. fmt.Println() is a function of the standard library (stdlib) to print some text to the standard output. It belongs to the fmt package.
    4. Having the import statement allows us to use the fmt package in the code, as we are doing with the fmt.Println() function.
  4. 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.
  5. You can also run the program from the embedded terminal by using
    1go run main.go

Simplest web server

  1. 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.
  2. Webservers listen to requests done to a given IP address and port. Let's define that in a constant inside of the main function:
    1const serverAddr string = "127.0.0.1:8081"
  3. 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: an http.ResponseWriter, used to produce the response, and an http.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.
    1http.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
    2})
  4. 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.
    1w.Write([]byte("HTTP Caracola"))
  5. 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().
    1http.ListenAndServe(serverAddr, nil)
  6. 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.
    1log.Fatal(http.ListenAndServe(serverAddr, nil))
  7. Compile and run. The codespace will offer to use a browser or open the port. You can ignore this for now.
  8. If you run the program from the terminal, open a second terminal using the ".
    1curl -i localhost:8081/

(De)Serialization

Unloading and deserializing task
  1. 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.
    1func createNote(w http.ResponseWriter, r *http.Request) {
    2}
  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.
    1type Note struct {
    2 Title string
    3 Text string
    4}
  3. 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:
    1type Scope struct {
    2 Project string
    3 Area string
    4}
    5
    6type Note struct {
    7 Title string
    8 Tags []string
    9 Text string
    10 Scope Scope
    11}
  4. 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.
  5. 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.
  6. 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.
    1var note Note
  7. 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 the decoder variable. I use the "short variable declaration" (:=), which declares and assigns value to the variable. In this case, Go is also doing type inference.
    1decoder := json.NewDecoder(r.Body)
  8. 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.
    1err := decoder.Decode(&note)
  9. 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.
    1if err := decoder.Decode(&note); err != nil {
    2}
  10. 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 the net/http package, writes to the response writer the provided error message, and sets the HTTP status.
    1http.Error(w, err.Error(), http.StatusBadRequest)
    2return
  11. 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).
    1fmt.Fprintf(w, "Note: %+v", note)
  12. Let's add this handler to our server. It will be used when a POST request is sent to the /notes path.
    1http.HandleFunc("POST /notes", createNote)
  13. Run this new version.
  14. 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.
    1curl -iX POST localhost:8081/notes
  15. 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.
    1curl -iX POST -d '{ "title": "Master plan", "tags": ["ai","users"], "text": "ubiquitous AI", "scope": {"project": "world domination", "area":"strategy"} }' localhost:8081/notes

Conclusion

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.
Start the Conversation

Facebook Icontwitter iconlinkedin icon
Rate this tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
This is part of a series
Implementing your REST API in Golang
More in this series
    Related
    Tutorial

    Concurrency and Gracefully Closing the MDB Client


    Sep 04, 2024 | 5 min read
    Tutorial

    HTTP Servers Persisting Data in MongoDB


    Sep 04, 2024 | 5 min read
    Tutorial

    Interact With MongoDB in an AWS Lambda Function Using Go


    Aug 29, 2024 | 6 min read
    Tutorial

    How to Build a Go Web App with Gin, MongoDB, and AI


    Aug 30, 2024 | 11 min read
    Table of Contents
    • HTTP basics with Go 1.22