All checks were successful
Build and copy to prod / build-and-copy (push) Successful in 2m4s
160 lines
4.1 KiB
Markdown
160 lines
4.1 KiB
Markdown
---
|
|
title: "Learning Go: Day Eleven"
|
|
date: 2024-05-13T08:00:00.0Z
|
|
tags:
|
|
- learning
|
|
- go
|
|
excerpt: "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:
|
|
|
|
```go
|
|
// 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.
|
|
|
|
```go
|
|
// 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.
|
|
|
|
```go
|
|
// 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.go`[^1], I can see that it succeeds:
|
|
|
|
```go
|
|
// main.go
|
|
|
|
func main() {
|
|
site := sites.Site{Name: "Lewisdale.dev", Url: "https://lewisdale.dev"}
|
|
site.Save(db)
|
|
|
|
ping.SendPing(db, site)
|
|
}
|
|
```
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```go
|
|
// 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:
|
|
|
|
```go
|
|
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 operation[^2]. Then `rows.Scan` will automagically insert the values to the variables I pass it, in the order the columns are read from the database[^3].
|
|
|
|
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:
|
|
|
|
```go
|
|
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 *`. |