Learning go: day nine
All checks were successful
Build and copy to prod / build-and-copy (push) Successful in 2m2s
All checks were successful
Build and copy to prod / build-and-copy (push) Successful in 2m2s
This commit is contained in:
parent
17d886a144
commit
49e27c9a3b
194
src/blog/posts/2024/5/learning-go-day-9.md
Normal file
194
src/blog/posts/2024/5/learning-go-day-9.md
Normal 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
|
Loading…
Reference in New Issue
Block a user