Learning go: day nine
All checks were successful
Build and copy to prod / build-and-copy (push) Successful in 2m2s

This commit is contained in:
Lewis Dale 2024-05-08 09:00:34 +01:00
parent 17d886a144
commit 49e27c9a3b

View File

@ -0,0 +1,194 @@
---
title: "Learning Go: Day Nine"
date: 2024-05-10T08:00:00.0Z
tags:
- learning
- go
excerpt: "Time to start getting myself connected to a database and write a bit of data"
---
Okay, so now I have a deployed project, and I'm ready to do things for real. To go back to my to-do list, here's where I am so far:
* ✅ Run a web server
* Render web pages with some dynamic content
* Store & read data from/to a database
* Send requets to a server & handle the responses
* Do all of the above on a periodic schedule
This isn't a prioritised list, so I'm going to move to the third item in the list, "Store & read data from/to a database".
## Choosing my database
It's Sqlite. It's not even a decision. It's got such a low barrier to setup, it's quick enough for the scale I'm working at, it's portable. I could also use PostgreSQL, but I don't want to.
## Setting up Sqlite
First-things-first, I need a way to communicate with my database. I already have the necessary drivers etc installed on my machine, so all I need is a Go library to handle the interface. I landed on [mattn/go-sqlite3](https://github.com/mattn/go-sqlite3), which seems to be pretty popular and actively maintained.
So I require the module using `go get github.com/mattn/go-sqlite3`, which adds a `require` block to my `go.mod` file, as well as creating a `go.sum` file[^1].
Now I'm going to try and do a test connection:
```go
// main.go
import (
"database/sql"
)
const createSitesTable = `CREATE TABLE IF NOT EXISTS sites (
id INTEGER NOT NULL PRIMARY KEY,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
name TEXT NOT NULL,
url TEXT NOT NULL
);`
func main() {
db, _ := sql.Open("sqlite3", "test.sqlite3")
db.Exec(createSitesTable)
// the rest of the main function...
}
```
This didn't work though. I got a nasty-looking segfault:
```bash
go run main.go
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x2 addr=0x0 pc=0x104c2ffb8]
```
So just having the sqlite module as a dependency isn't enough, I need to import it. But if I update the `import` statement to include `github.com/mattn/go-sqlite3`, the auto-formatter just removes it immediately. Instead, I have to prefix the import with `_` so that it remains:
```go
// main.go
import (
"database/sql"
_ "github.com/mattn/go-sqlite3"
)
```
Now this works, and when I do `go run main.go` it creates a `test.sqlite3` file, with the table defined:
```shell
sqlite3 test.sqlite3
SQLite version 3.43.2 2023-10-10 13:08:14
Enter ".help" for usage hints.
sqlite> .schema sites
CREATE TABLE sites (
id INTEGER NOT NULL PRIMARY KEY,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
name TEXT NOT NULL,
url TEXT NOT NULL
);
sqlite>
```
## Defining the sites models
Okay, now I'm going to create a data model to represent a stored Site. First, I created a new package, `/sites/sites.go`, and then made a `CreateTable` function that just ran the query from the previous section:
```go
// sites/sites.go
package sites
import (
"database/sql"
"fmt"
)
const createSitesTable = `CREATE TABLE IF NOT EXISTS sites (
id INTEGER NOT NULL PRIMARY KEY,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
name TEXT NOT NULL,
url TEXT NOT NULL
);`
func CreateTable(db *sql.DB) {
if _, err := db.Exec(createSitesTable); err != nil {
fmt.Println(fmt.Errorf(err.Error()))
}
}
```
Then in my main function, I can just do:
```go
// main.go
import (
// ...
"lewisdale.dev/oopsie/sites"
)
func main() {
// ...
sites.CreateTable(db)
}
```
Now, I want to define a model, which I'm going to do using a `struct`:
```go
// sites/sites.go
type Site struct {
id uint64
created_at uint64
Name string
Url string
}
```
The `Name` and `Url` arguments are capitalised because I want them to be publicly accessible. This caught me out when I first tried to instantiate the struct with named parameters. I don't want `id` or `created_at` to be public fields - right now, at least.
Now I can add a method to my struct that will allow me to save the model in the database:
```go
// sites/sites.go
func (s *Site) Save(db *sql.DB) {
if s.id != 0 {
query := `UPDATE SITES
SET
name=?,
url=?
WHERE id =?
`
db.Exec(query, s.Name, s.Url, s.id)
} else {
query := `INSERT INTO SITES (name, url) VALUES (?, ?)`
db.Exec(query, s.Name, s.Url)
}
}
```
Really, I want these sites to be unique on the URL, which would also make for easier upserts, but for now this is fine. I can then, in `main.go`, trigger this function to insert a new row to my table:
```go
// main.go
func main() {
site := sites.Site{Name: "Lewisdale.dev", Url: "https://lewisdale.dev"}
site.Save(db)
}
```
And I can see it's been inserted:
```bash
sqlite3 test.sqlite3
SQLite version 3.43.2 2023-10-10 13:08:14
Enter ".help" for usage hints.
sqlite> SELECT * FROM sites;
1|2024-05-08 07:56:52|Lewisdale.dev|https://lewisdale.dev
```
I think that's where I'll leave today's post, but things are starting to get a bit more interesting now at least! I'm going to have to work out how to properly test this stuff soon.
[^1]: Apparently this is for the checksums of my dependencies