lewisdale.dev/src/blog/posts/2024/5/learning-go-day-11.md
Lewis Dale 406383e3c1
All checks were successful
Build and copy to prod / build-and-copy (push) Successful in 2m4s
Finish tomorrow's WeblogPoMo post
2024-05-12 21:48:27 +01:00

4.1 KiB

title date tags excerpt
Learning Go: Day Eleven 2024-05-13T08:00:00.0Z
learning
go
Now it's time to actually try and send a "ping" to a website

So, next up on my task list is to start populating my database with data. So to do that I want to be able to send a request to a given website, and then store the success or failure status based on the response.

Sending a request

It turns out that this is actually quite simple to do. The net/http module has functions to do this, so all I need to do is:

// ping/ping.go

import (
	"net/http"
)

func SendPing(db *sql.DB, site sites.Site) {
	response, error := http.Get(site.url)

    // Do something with the response or error
}

Easy enough!

Store the result

Now all I need to do is save the output to the database. I do that by using the inline if-statement syntax, and assigning ping.Status depending on whether or not there is an error present. I don't actually care about the response, I just care whether or not the call succeeded.

// ping/ping.go

func SendPing(db *sql.DB, site sites.Site) {
	p := Ping{
		Site: site,
	}

	if _, err := http.Get(site.Url); err != nil {
		p.Status = Failure
	} else {
		p.Status = Success
	}

	p.Save(db)
}

And as a little refactor, I can default the Status to Success, and get rid of the else- branch.

// ping/ping.go

func SendPing(db *sql.DB, site sites.Site) {
	p := Ping{
		Site: site,
        Status: Success
	}

	if _, err := http.Get(site.Url); err != nil {
		p.Status = Failure
	}

	p.Save(db)
}

Now, if I write a quick test in my main.go1, I can see that it succeeds:

// main.go

func main() {
    site := sites.Site{Name: "Lewisdale.dev", Url: "https://lewisdale.dev"}
	site.Save(db)

	ping.SendPing(db, site)
}
sqlite> SELECT ping.id, ping.site, ping.timestamp, statuses.name FROM ping LEFT JOIN statuses ON (ping.status = statuses.id);
1|https://lewisdale.dev|2024-05-10 05:57:12|Success

And if I then force a failure, it should also work:

5|https://notreal.tld|2024-05-10 08:00:46|Failure

Adding some output

Right, now to actually output the values in the database. First of all, I've defined the Struct for the response:

// ping/ping.go

type PingResponse struct {
	Site      sites.Site
	Timestamp string
	Status    string
}

And now I've added a List function that reads the data I need from the database, and places it into an array slice of PingResponse values:

func List(db *sql.DB) []PingResponse {
	rows, err := db.Query(`SELECT sites.url as url, sites.name, ping.timestamp as timestamp, statuses.name as status FROM ping
		JOIN sites ON ping.site = sites.url
		JOIN statuses ON ping.status = statuses.id
	ORDER BY timestamp DESC`)

	if err != nil {
		panic(err)
	}
	defer rows.Close()

	pings := make([]PingResponse, 0)

	for rows.Next() {
		p := PingResponse{}
		rows.Scan(&p.Site.Url, &p.Site.Name, &p.Timestamp, &p.Status)
		pings = append(pings, p)
	}

	return pings
}

The interesting parts here are the defer statement, and rows.Scan. Defer queues that call up until after the function has executed, it's just a way of saying "I will be doing this at the end regardless" as a cleanup operation2. Then rows.Scan will automagically insert the values to the variables I pass it, in the order the columns are read from the database3.

Then finally, I can update my handler function so that it uses json.Marshal to convert the pings to JSON, and output them to the browser:

http.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
		pings := ping.List(db)

		if output, err := json.Marshal(pings); err != nil {
			w.Write([]byte(err.Error()))
			w.WriteHeader(http.StatusInternalServerError)
			return
		} else {
			w.Header().Set("Content-Type", "application/json")
			w.Write(output)
		}
})

And that works! You can see it at https://oopsie.lewisdale.dev, with (hopefully) some actual output.


  1. Yes, despite what I said in another post I've not written any actual tests. I'm human, alright? ↩︎

  2. I think ↩︎

  3. This is where it helps to be explicit with what is selected and avoid SELECT *. ↩︎