DEV Community

Cover image for Attempting to Learn Go - Building a Downloader Part 02
Steve Layton
Steve Layton

Posted on • Originally published at shindakun.glitch.me on

Attempting to Learn Go - Building a Downloader Part 02

Last time we outlined our plan to create a simple Go server that would download remote files. What I'd like to do this time is to begin to write some actual code. I'm going to try and document exactly how I put the program together along with any references I used. I've also tried to be explicit with regards to naming my variables. For instance, using response http.ResponseWriter, request *http.Request instead of w http.ResponseWriter, r *http.Request. The hope is that anyone of any skill will be able to follow the code.

Pass One

Our first task is simply to get some code on the screen. We're going to define our package, main and a single import fmt (format) so we can print to standard output.

package main

import "fmt"

We'll use the JSON object we defined previously and JSON-to-Go website to quickly create our struct. I recommend bookmarking this site, if you are going to work with JSON in Go it comes in very handy.

type download struct {  
  Title string `json:"title"`
  Location string `json:"location"`
}

In our main function, for now, we're first going to simply print "Downloader" at startup. After that, we're going to create a test struct which will then be printed.

func main() {  
  fmt.Printf("Downloader")

  download := download{
    Title: "title-test",
    Location: "location-test",
  }

  fmt.Printf("%v", download)
}

Which, when run, results in the following output.

$ go run downloader.go
Downloader  
{title-test location-test}

Pass Two

The first thing to take note of is that we are using two new imports. io/ioutil and net/http are both parts of the Go standard library and will help take care of some of the functionality. The standard library gives the developer the raw tools needed to create many different programs. All they need to do is learn how to put it together. And as we'll see in a future post if a standard library package doesn't exist for a function there is a good chance someone in the community has thought of that.

package main

import (  
  "fmt"
  "io/ioutil"
  "net/http"
)

type download struct {  
  Title string `json:"title"`
  Location string `json:"location"`
}

We come now to our first new function, status. Currently, requesting "/" on the server will result in a 200 response with a body of "Hello!". Eventually, we will update this to return a little bit of server information for service health check purposes.

func status(response http.ResponseWriter, request *http.Request) {  
  fmt.Fprintf(response, "Hello!")
}

The next new function handleDownloadRequest is going to be the main focus of the server portion of our program. In its current form, it will accept a request on "/download" and print the body from the request to the console and the word "Download!" to the browser.

func handleDownloadRequeset(response http.ResponseWriter, request *http.Request) {  
  r, err := ioutil.ReadAll(request.Body)
  if err != nil {
    fmt.Println(err)
  }
  defer request.Body.Close()
  fmt.Println(string(r))

  fmt.Fprintf(response, "Download!")
}

To make use of our new functions we've updated main to set up the HTTP handlers. Finally, we start the server listening on port 3000.

func main() {  
  fmt.Println("Downloader")

  http.HandleFunc("/", status)
  http.HandleFunc("/download", handleDownloadRequest)
  http.ListenAndServe(":3000", nil)
}

Pass Three

The majority of changes this time around occur in the handleDownloadRequest function. However, if you look closely you'll see that I've also added encoding/json and log as imports. Being a server application I wanted to use the built-in features of log to print to the console. I'm not sure if I'm going to redirect it to file or just leave it on the console at this point though.

package main

import (  
  "encoding/json"
  "fmt"
  "io/ioutil"
  "log"
  "net/http"
)

type download struct {  
  Title string `json:"title"`
  Location string `json:"location"`
}

func status(response http.ResponseWriter, request *http.Request) {  
  fmt.Fprintf(response, "Hello!")
}

Currently, the heart of our program resides here. We have what looks to be some significant changes. First, we are creating a struct called downloadReqest to hold our incoming title and URL. I've also added some better error handling, with actual responses to the browser when appropriate.

func handleDownloadRequest(response http.ResponseWriter, request *http.Request) {  
  var downloadRequest download
  r, err := ioutil.ReadAll(request.Body)
  if err != nil {
    http.Error(response, "bad request", 400)
    log.Println(err)
    return
  }
  defer request.Body.Close()

Assuming the initial request is OK and we are able to read the body, we then need to get the request JSON into our struct. json.Unmarshal will take care of the heavy lifting for us. If not we can return an error to the browser, here we include the error message from json.Unmarshal in the output.

err = json.Unmarshal(r, &downloadRequest)
  if err != nil {
    http.Error(response, "bad request: "+err.Error(), 400)
    log.Println(err)
    return
  }
  log.Printf("%#v", downloadRequest)

  fmt.Fprintf(response, "Download!")
}

func main() {  
  log.Println("Downloader")

  http.HandleFunc("/", status)
  http.HandleFunc("/download", handleDownloadRequest)
  http.ListenAndServe(":3000", nil)
}

Not bad for a little bit of work. We have gone from a basic skeleton to a server in no time. Starting tomorrow and our next revision we'll update the handleDownloadRequest function by adding a getFile function that we can call to get our actual file.

Until next time...


You can find the code for this and most of the other Attempting to Learn Go posts in the repo on GitHub.



Latest comments (0)