From 49e27c9a3b2934d92a48d74c0bec701964d537a2 Mon Sep 17 00:00:00 2001 From: Lewis Dale Date: Wed, 8 May 2024 09:00:34 +0100 Subject: [PATCH] Learning go: day nine --- src/blog/posts/2024/5/learning-go-day-9.md | 194 +++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 src/blog/posts/2024/5/learning-go-day-9.md diff --git a/src/blog/posts/2024/5/learning-go-day-9.md b/src/blog/posts/2024/5/learning-go-day-9.md new file mode 100644 index 0000000..68e75d3 --- /dev/null +++ b/src/blog/posts/2024/5/learning-go-day-9.md @@ -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 \ No newline at end of file