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