Merge pull request 'Ooops should have been working on master' (#1) from drew/sql-it into master

Reviewed-on: #1
drew/sql-it
Drew Bednar 4 months ago
commit 001756b7e3

@ -0,0 +1,7 @@
# Basic Data Structures
These are basic implementations of standard data structures. They are not thread safe implementations. To avoid race conditions you would need to use a mutex (mutual exclusion lock) to lock the data structure before any reads or writes. The `sync` package has a `RWMutex` that can be used.
## Example of using Mutex

@ -0,0 +1,3 @@
module datastructures
go 1.23.1

@ -0,0 +1,41 @@
package datastructures
import "errors"
// Queue a First in First out data structure.
type Queue struct {
elements []any
}
// Enqueue adds and element to the queue
func (q *Queue) Enqueue(el any) {
q.elements = append(q.elements, el)
}
// Dequeue removes an element from the queue
func (q *Queue) Dequeue() (any, error) {
if q.IsEmpty() {
return nil, errors.New("empty queue")
}
el := q.elements[0]
q.elements = q.elements[1:]
return el, nil
}
// Peek retrieves a copy of the next element in the queue.
func (q *Queue) Peek() (any, error) {
if q.IsEmpty() {
return nil, errors.New("empty queue")
}
return q.elements[0], nil
}
// IsEmpty checks if the queue has any elements
func (q *Queue) IsEmpty() bool {
return q.Size() == 0
}
// Size checks the size of the queue
func (q *Queue) Size() int {
return len(q.elements)
}

@ -0,0 +1,91 @@
package datastructures
type Set struct {
elements map[any]bool
}
func (s *Set) Add(el any) {
s.elements[el] = true
}
func (s *Set) Remove(el any) {
delete(s.elements, el)
}
func (s *Set) IsEmpty() bool {
return s.Size() == 0
}
func (s *Set) Size() int {
return len(s.elements)
}
func (s *Set) Has(el any) bool {
_, ok := s.elements[el]
return ok
}
func (s *Set) Difference(set Set) Set {
d := s.Copy()
for k := range d.elements {
if set.Has(k) {
d.Remove(k)
}
}
return d
}
func (s *Set) IsSubset(t Set) bool {
for k := range s.elements {
if !t.Has(k) {
return false
}
}
return true
}
func (s *Set) List() []any {
list := make([]any, s.Size())
for k := range s.elements {
list = append(list, k)
}
return list
}
func (s *Set) Copy() Set {
c := NewSet()
for k := range s.elements {
c.Add(k)
}
return c
}
// NewSet creates a new instance of Set
func NewSet() Set {
return Set{elements: make(map[any]bool)}
}
// Union determines the OR relationship on all sets under consideration
func Union(sets ...Set) Set {
u := sets[0].Copy()
for _, set := range sets[1:] {
for k := range set.elements {
u.Add(k)
}
}
return u
}
// Intersection determines the AND relationship of all sets under consideration
func Intersection(sets ...Set) Set {
i := sets[0].Copy()
for k := range i.elements {
for _, set := range sets[1:] {
if !set.Has(k) {
i.Remove(k)
}
}
}
return i
}

@ -0,0 +1,66 @@
package datastructures
import "errors"
// Stack a Last in First out data structure.
type Stack struct {
elements []any
}
// Push add an element to the top of the stack
//
// Parameters:
//
// any: an element to add to the top of the stack.
func (s *Stack) Push(el any) {
s.elements = append(s.elements, el)
}
// Pop removes and returns the top element of the stack.
//
// Returns:
//
// any: The top element of the stack
//
// error: If the stack is found to be empty
func (s *Stack) Pop() (any, error) {
if s.IsEmpty() {
return nil, errors.New("empty stack")
}
el := s.elements[len(s.elements)-1]
s.elements = s.elements[:len(s.elements)-1]
return el, nil
}
// Peek checks the top item in the stack.
//
// Returns:
//
// any: a copy of the top element
//
// error: An error if the stack is empty
func (s *Stack) Peek() (any, error) {
if s.IsEmpty() {
return nil, errors.New("empty stack")
}
return s.elements[len(s.elements)-1], nil
}
// IsEmpty checks if the stack contains any elements.
//
// Returns:
//
// bool: True is the stack is empty. False if the stack contains elements.
func (s *Stack) IsEmpty() bool {
return s.Size() == 0
}
// Size checks this size of the stack
//
// Returns:
//
// int: The number of elements on the stack
func (s *Stack) Size() int {
return len(s.elements)
}

@ -0,0 +1,301 @@
# Using the Debugger
Go doesn't ship with a debugger but you can use the delve package instead.
https://www.youtube.com/watch?v=UA0SirX6Siw
```
go get github.com/go-delve/delve/cmd/dlv
```
```
dlv version
Delve Debugger
Version: 1.23.1
Build: $Id: 2eba762d75437d380e48fc42213853f13aa2904d $
```
## Using dlv
From within a go module run:
```
dlv debug
Type 'help' for list of commands.
(dlv)
```
Now set a breakpoint on the main function
```
break main.main
```
This breaks not at your main function but in the go runtime.
Now you can do something like set a break point at a line number in a file
```
break main.go:20
```
Now we can continue until we hit a breakpoint
```
c or continue
> [Breakpoint 1] main.main() ./main.go:19 (hits goroutine(1):1 total:1) (PC: 0x4ae66e)
14:
15: func Three() {
16: fmt.Println("Three!")
17: }
18:
=> 19: func main() {
20: msg := "Hello debugger"
21: One()
22: fmt.Println(msg)
23:
24: }
```
Now we can check for locals
```
locals
(no locals)
```
No surprising there are no local variables at this point.
```
n
```
Will advance us to the next line. But again no locals because that hasn't been executed yet. So `n` again.
```
locals
msg = "Hello debugger"
```
```
ls
> main.main() ./main.go:21 (PC: 0x4ae687)
16: fmt.Println("Three!")
17: }
18:
19: func main() {
20: msg := "Hello debugger"
=> 21: One()
22: fmt.Println(msg)
23:
24: }
```
Now we can step into our function with `s`
```
s
> main.One() ./main.go:5 (PC: 0x4ae48a)
1: package main
2:
3: import "fmt"
4:
=> 5: func One() {
6: Two()
7: fmt.Println("One!")
8: }
9:
10: func Two() {
```
You can se now we have stepped into the function.
We can `n` and `s` into `Two()`.
```
> main.Two() ./main.go:10 (PC: 0x4ae52a)
5: func One() {
6: Two()
7: fmt.Println("One!")
8: }
9:
=> 10: func Two() {
11: Three()
12: fmt.Println("Two!")
13: }
14:
15: func Three() {
```
How about we look at the stack trace at this point
```
stack
0 0x00000000004ae52a in main.Two
at ./main.go:10
1 0x00000000004ae493 in main.One
at ./main.go:6
2 0x00000000004ae68c in main.main
at ./main.go:21
3 0x000000000043b647 in runtime.main
at /usr/local/go/src/runtime/proc.go:272
4 0x0000000000474081 in runtime.goexit
at /usr/local/go/src/runtime/asm_amd64.s:1700
```
Let's `n` and `s` one more time and check the stack trace.
```
0 0x00000000004ae5ca in main.Three
at ./main.go:15
1 0x00000000004ae533 in main.Two
at ./main.go:11
2 0x00000000004ae493 in main.One
at ./main.go:6
3 0x00000000004ae68c in main.main
at ./main.go:21
4 0x000000000043b647 in runtime.main
at /usr/local/go/src/runtime/proc.go:272
5 0x0000000000474081 in runtime.goexit
at /usr/local/go/src/runtime/asm_amd64.s:1700
```
If we needed to we could us `frame` to go back into the stack and and we can inspect the state of code at that point.
```
frame 3
locals
```
But then we need to move back to `frame 0` and can continue debugging.
```
(dlv) ls
> main.Two() ./main.go:11 (PC: 0x4ae52e)
6: Two()
7: fmt.Println("One!")
8: }
9:
10: func Two() {
=> 11: Three()
12: fmt.Println("Two!")
13: }
14:
15: func Three() {
16: fmt.Println("Three!")
```
Instead of stepping to `Three()` let's step out of Two.
```
(dlv) so
Three!
Two!
> main.One() ./main.go:7 (PC: 0x4ae493)
Values returned:
2:
3: import "fmt"
4:
5: func One() {
6: Two()
=> 7: fmt.Println("One!")
8: }
9:
10: func Two() {
11: Three()
12: fmt.Println("Two!")
```
We see out stdout messages now there is one to go.
```
(dlv) stack
0 0x00000000004ae493 in main.One
at ./main.go:7
1 0x00000000004ae68c in main.main
at ./main.go:21
2 0x000000000043b647 in runtime.main
at /usr/local/go/src/runtime/proc.go:272
3 0x0000000000474081 in runtime.goexit
at /usr/local/go/src/runtime/asm_amd64.s:1700
```
We can continue this until our process returns. But when we are back in our main function. Let's look at locals one more time.
```
17: }
18:
19: func main() {
20: msg := "Hello debugger"
21: One()
=> 22: fmt.Println(msg)
23:
24: }
locals
msg = "Hello debugger"
```
We can't change this variable because Delve wants to avoid operations itself that involves memory allocations, but if we had a
```
func setMsg(value string) {
msg = value
}
```
We could use `call` to set that variable
```
call setMsg("dirp")
```
To close it could just `continue` until the process exits.
```
(dlv) c
Process 29036 has exited with status 0
```
## Wait there is more
Help has tons of stuff
```
help
```
Like
```
restart
```
Restarts the program of course. Our current breakpoints are still set.
```
(dlv) print msg
"Hello debugger"
```
And hey this time we used print to check a value.
Technically you can edit code with `edit`.
## Debugging goroutines

@ -0,0 +1,3 @@
module debugger
go 1.23.1

@ -0,0 +1,51 @@
package main
import (
"fmt"
"sync"
)
func One() {
Two()
fmt.Println("One!")
}
func Two() {
Three()
fmt.Println("Two!")
}
func Three() {
fmt.Println("Three!")
}
// Used in the simple example
// func main() {
// msg := "Hello debugger"
// One()
// fmt.Println(msg)
// }
// Used for Goroutine debugging
func printMe(i int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("I'm a go routine %d\n", i)
}
func main() {
i := 0
var wg sync.WaitGroup
wg.Add(9)
for i < 10 {
go func(rI int) {
printMe(rI, &wg)
}(i)
i++
}
wg.Wait()
}

@ -0,0 +1,6 @@
# Learning SQL DB Access
Resources:
http://go-database-sql.org/index.html It's the missing guide to the docs "how" to use the database/sql package.

@ -0,0 +1,23 @@
# Regular SQLite in Go
## Install the Migrations Tool
This will in stall the migrate tool for our system.
```
go install -tags 'sqlite3' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
```
You could also use golang-migrate as a library and migrate your DB in code. For that you would need to install that package as part of your module dependencies.
```
go get github.com/mattn/go-sqlite3
```
## Running Migrations
### Using the CLI
```
migrate -database sqlite3://./data/trysqlite.db -path ./migrations
```

@ -0,0 +1,5 @@
module try-sqlite
go 1.21.0
require github.com/mattn/go-sqlite3 v1.14.22 // indirect

@ -0,0 +1,2 @@
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=

@ -0,0 +1,272 @@
package main
import (
"database/sql"
"flag"
"log"
_ "github.com/mattn/go-sqlite3"
)
func showTransactionStmtClose(p *sql.Stmt) {
log.Println("Closing statement!")
p.Close()
}
func main() {
log.Println("Hello, world!")
db_path := flag.String("database", "./data/trysqlite.db", "Path to a Sqlite3 database")
flag.Parse()
// TODO figure out what query string options to be using
full_database_path := "file:" + *db_path + "?cache=shared"
log.Printf("Using Database: %s", full_database_path)
// db is a Handle backed by a go database/sql connection pool. It is not a connection in of itself, nor is it the actual connection pool
// The first connection is made lazily by the handle, and this doesn't validate any of the connection parameters either at this point.
// It simply prepares the abstraction for use.
db, err := sql.Open("sqlite3", full_database_path)
if err != nil {
log.Fatalf("Failed to connect to sqlite database: %s", full_database_path)
}
// It is idiomatic to defer db.Close() if the sql.DB should not have a lifetime beyond the scope of the function.
//Although its idiomatic to Close() the database when youre finished with it, the sql.DB object is designed to be long-lived.
//Dont Open() and Close() databases frequently. Instead, create one sql.DB object for each distinct datastore you need to access,
// and keep it until the program is done accessing that datastore.
// Pass it around as needed, or make it available somehow globally, but keep it open.
// And dont Open() and Close() from a short-lived function. Instead, pass the sql.DB
// into that short-lived function as an argument.
//If you dont treat the sql.DB as a long-lived object, you could experience problems such as poor reuse and sharing of connections,
// running out of available network resources, or sporadic failures due to a lot of TCP connections remaining in TIME_WAIT status.
// Such problems are signs that youre not using database/sql as it was designed.
defer db.Close()
// If you want to check right away that the db is available and accessible you can do this and check for errs
err = db.Ping()
if err != nil {
log.Fatal("DB Ping failed. Check database and database connection parameters")
}
var (
id int
subject string
todo string
)
// db.Query vs db.Exec. If a function name includes Query, it is designed to ask a question of the database,
// and will return a set of rows, even if its empty. Statements that dont return rows should not use Query functions;
// they should use Exec()
rows, err := db.Query("SELECT subject FROM todos")
if err != nil {
log.Fatal(err)
}
// It is important to defer closing a row object also. Why? It will release the memory and the network connection. So it
// also prevents resource leaks. So you are a good steward, you clean up after yourself.
// You don't wait for the garbage collector to do it.
// as long as theres an open result set (represented by rows), the underlying connection is busy and cant be used for
//any other query. It's busy because sql.Rows objects don't retrieve all results a query returns. It does it lazily to prevent over runing memory.
// it depends on the driver but there is usually and internal buffer that stores results.
// Also never defer in a loop. Defer only runs on function exit.
defer rows.Close()
// Iterate through each. Internally rows.Next will hit an EOF error it will call rows.close() for you freeing up the connection resources.
// but you shouldn't rely on that alone. rows.Close() will run on function exit, but this example is a Main func, so it's not realistic
// still get in the habit
for rows.Next() {
err = rows.Scan(&subject)
if err != nil {
log.Fatal(err)
}
log.Printf("Subject is %s", subject)
}
// Dont just assume that the loop iterates until youve processed all the rows. Always check.
err = rows.Err()
if err != nil {
log.Fatal(err)
}
// You could also clean up at this point. The call is supposed to be idempotent, but defer is more idiomatic.
// You don't want to call close if there was an Error because of that could cause a Panic.
rows.Close()
// If never want to do string concat from user input into a sql statement you run unless you like sql injection attacks
// expects to return one row. If no row it will delay throwing error until Scan is called
row := db.QueryRow("SELECT id, subject, todo FROM todos WHERE id = ?", 1)
// Scan does the heavily lifting for you when casting from db type to go type. It will cast based on the variable type.
// failures of course will be returned as part of the err
err = row.Scan(&id, &subject, &todo)
if err != nil {
log.Fatal(err)
}
log.Printf("id: %d, subject: %s, todo: %s", id, subject, todo)
// This should error on scan
// We can also use this short hand!
err = db.QueryRow("SELECT * FROM todos WHERE id = ?", 100_000).Scan(&id, &subject, &todo)
if err != nil {
log.Print(err.Error())
}
// There is no row.Close(). Rows objects have a Close() method. No explicit closing required.
//Prepared Queries
// If you will be using the same query over and over you should just prepare the query.
// $N (e.g. $1, $2, etc...) is the postgres query param which SQLite can use too ? is mysql's
//Under the hood, db.Query() actually prepares, executes, and closes a prepared statement
// Thats three round-trips to the database.
stmt, err := db.Prepare("SELECT todo as other_todo FROM todos WHERE id = $1")
if err != nil {
log.Fatal(err)
}
//This prepared statement *sql.Stmt is tied to the database connection from which it was created, and it holds onto that connection until you call stmt.Close()
defer stmt.Close()
// You can also call with just params the .QueryRow method since the statement is already prepared
var other_todo string
err = stmt.QueryRow(1).Scan(&other_todo)
if err != nil {
log.Fatal(err)
}
log.Printf("Other Todo: %s", other_todo)
stmt.Close()
// MODIFYING DATA
// Use db.Exec() with a prepared statement when INSERTING, UPDATING, or DELETING
// Or another statement that doesn't return rows.
stmt, err = db.Prepare("INSERT INTO todos (subject, todo) VALUES ($1, $2)")
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
// stmt.Exec returns a sql.Result. That also has access to the statement metadata
// use _ if you don't care about the result and only want to check the err.
// Exec will NOT reserve a connection. unlike db.Query which returns a sql.Rows that
// will hold on to a connection until .Close() is called.
res, err := stmt.Exec("programming", "learn golang database queries!")
if err != nil {
log.Print("Dirp...")
log.Fatal(err)
}
lastId, err := res.LastInsertId()
if err != nil {
log.Fatal(err)
}
log.Printf("Last inserted id: %d", lastId)
rowCnt, err := res.RowsAffected()
if err != nil {
log.Fatal(err)
}
log.Printf("The number of rows affected: %d", rowCnt)
// close insert
stmt.Close()
stmt, err = db.Prepare("DELETE FROM todos WHERE subject = 'programming'")
if err != nil {
log.Fatal(err)
}
res, err = stmt.Exec()
if err != nil {
log.Fatal(err)
}
rowCnt, err = res.RowsAffected()
if err != nil {
log.Fatal(err)
}
log.Printf("The number of rows affected: %d", rowCnt)
// TRANSACTIONS
//In Go, a transaction is essentially an object that reserves a connection to the datastore.
//It lets you do all of the operations weve seen thus far, but guarantees that theyll be
//executed on the same connection.
// Starts with db.Begin() returning a sql.Tx and ends with a .Commit() or .Rollback() on the results sql.Tx
tx, err := db.Begin()
if err != nil {
log.Fatal(err)
}
// Prepared statement in a transaction are bound to that transaction exclusively.
// DO NOT use sql BEGIN or COMMIT in a sql.Tx object. It could leave the DB and client in different states.
// or mess havoc with the connection pool and connection lifetime
insert_stmt, err := tx.Prepare("INSERT INTO todos (subject, todo) VALUES ($1, $2)")
if err != nil {
log.Fatal(err)
}
defer insert_stmt.Close() // This closes at the end of the transaction, not the end of the function.
_, _ = insert_stmt.Exec("dirp", "just a place holder")
// There is no Close method on a transaction. It is closed by using .Commit or .Rollback
tx.Rollback()
// PREPARED STATEMENTS IN MORE DEPTH
//At the database level, a prepared statement is bound to a single database connection.
//The typical flow is that the client sends a SQL statement with placeholders to the server
//for preparation, the server responds with a statement ID, and then the client executes
//the statement by sending its ID and parameters.
// THE ABOVE DOES NOT HAPPEN IN database/sql. Instead the statement is managed at the driver level
// Go doesn't expose connections directly to users. The sql.Stmt object remembers which connection
// from the pool it is associated with.
//Know that if a connection is busy it may automatically get a new connection and re-prepare the
// statement on a new connection. It could cause more connections than you think, more statement
// recreation, or even hitting a server limit on prepared statements.
// NOTE db.Query(sql, param1, param2) will create a prepared statement under the hood.
// If you dont want to use a prepared statement, you need to use fmt.Sprint() or similar to
//assemble the SQL, and pass this as the only argument to db.Query() or db.QueryRow() And your driver
//needs to support plaintext query execution,
// NOTE prepared statements on Tx are bound exclusively to the Tx. You cannot use a prepared statement
// created by db.Prepare nor can you use a Tx.Prepare statement with a db.Query. Tx.Stmt() can be used
// to copy a db.Prepared statement into a transaction, but don't do this.
// HANDLING ERRORS
// You should always explicitly close a sql.Rows result set. If rows.Close() returns an error, its
// unclear what you should do, so logging the error message and ignoring or panic-ing might be the only sensible
// thing to do.
// Use sql.ErrNoRows in the event that no data is returned.
// rememeber db.QueryRow defers errors till Scan is called.
err = db.QueryRow("select subject from todos where id = $1", 100_000).Scan(&subject)
if err != nil {
if err == sql.ErrNoRows {
// there were no rows, but otherwise no error occurred
log.Println("No rows were returned! But that is okay. We expected that could happen")
} else {
log.Fatal(err)
}
}
// Each driver has it's own error types
}

@ -0,0 +1,2 @@
PRAGMA foreign_keys=1;
CREATE TABLE todos (id INTEGER PRIMARY KEY AUTOINCREMENT, subject TEXT, TODO TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP);

@ -0,0 +1 @@
ALTER TABLE todos ADD completed BOOL DEFAULT false;

@ -19,3 +19,15 @@ I've taken this project a bit further then the article. Follow on work can inclu
- [ ] Runs test suite
- [ ] Tarball release artifact on Promote
- [ ] Deb release artifact on Promote
## Adding Libsql to a project
First install the driver
```
go get github.com/tursodatabase/go-libsql
```
We will only use a local file for this project. We will will use `GO_WIKI_DB` environment variable to determine where the SQLite file is located. It will default to ./data like the previous file based system.

@ -1,3 +1,10 @@
module gowiki
go 1.21.0
require (
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06 // indirect
github.com/tursodatabase/go-libsql v0.0.0-20240429120401-651096bbee0b // indirect
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect
)

@ -0,0 +1,8 @@
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06 h1:JLvn7D+wXjH9g4Jsjo+VqmzTUpl/LX7vfr6VOfSWTdM=
github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06/go.mod h1:FUkZ5OHjlGPjnM2UyGJz9TypXQFgYqw6AFNO1UiROTM=
github.com/tursodatabase/go-libsql v0.0.0-20240429120401-651096bbee0b h1:R7hev4b96zgXjKbS2ZNbHBnDvyFZhH+LlMqtKH6hIkU=
github.com/tursodatabase/go-libsql v0.0.0-20240429120401-651096bbee0b/go.mod h1:TjsB2miB8RW2Sse8sdxzVTdeGlx74GloD5zJYUC38d8=
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU=
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=

@ -0,0 +1,36 @@
package concurrency
type WebsiteChecker func(string) bool
type result struct {
string
bool
}
func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool {
results := make(map[string]bool)
resultChannel := make(chan result)
for _, url := range urls {
// anonymous function call required because go routines must call
// a function to start. An anonymous function maintains access to
// the lexical scope in which they are defined - all the variables
// that are available at the point when you declare the anonymous
//function are also available in the body of the function.
// url := url // create a new variable to avoid capturing the loop variable.
// go func() {
// results[url] = wc(url)
// }()
go func(u string) {
resultChannel <- result{u, wc(u)} // send statement
}(url) // or just pass by value to give the func it's own url
}
// Improvement idea. We could use a wait group here. Instead we are blocking
// each go routine because we are using an unbuffered channel.
for i := 0; i < len(urls); i++ {
r := <-resultChannel // receive statement
results[r.string] = r.bool
}
return results
}

@ -0,0 +1,48 @@
package concurrency
import (
"reflect"
"testing"
"time"
)
func mockWebsiteChecker(url string) bool {
return url != "waat://furhurterwe.geds"
}
func slowStubWebsiteChecker(_ string) bool {
time.Sleep(20 * time.Millisecond)
return true
}
// What does testing.B do?
func BenchmarkCheckWebsites(b *testing.B) {
urls := make([]string, 100)
for i := 0; i < len(urls); i++ {
urls[i] = "a url"
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
CheckWebsites(slowStubWebsiteChecker, urls)
}
}
func TestWebsites(t *testing.T) {
websites := []string{
"http://google.com",
"http://hackaday.com",
"waat://furhurterwe.geds",
}
want := map[string]bool{
"http://google.com": true,
"http://hackaday.com": true,
"waat://furhurterwe.geds": false,
}
got := CheckWebsites(mockWebsiteChecker, websites)
if !reflect.DeepEqual(want, got) {
t.Fatalf("Wanted %v, got %v", want, got)
}
}

@ -0,0 +1,9 @@
# Concurrency
https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/concurrency
This was an interesting chapter. The high-level takeaways:
- An anonymous function maintains access to the lexical scope in which they are defined
- Go can help identify race conditions with [race detector](https://blog.golang.org/race-detector) `go test -race`
- Coordinating go routines can be accomplished with channels. Channels are a data structure that can both receive and send values. This allows cross go routine communication. Channels have a type, and you will commonly see structs passed around.

@ -0,0 +1,3 @@
module concurrency
go 1.22.5

@ -0,0 +1,3 @@
module my_select
go 1.22.5

@ -0,0 +1,66 @@
package my_select
import (
"fmt"
"net/http"
"time"
)
var defaultTimeout time.Duration = 10 * time.Second
func Racer(a, b string) (winner string, err error) {
return ConfigurableRacer(a, b, defaultTimeout)
}
func ConfigurableRacer(a string, b string, timeout time.Duration) (winner string, err error) {
// aDuration := measureResponseTime(a)
// bDuration := measureResponseTime(b)
// if aDuration < bDuration {
// return a
// }
// return b
select {
// Here we are simply waiting on the channel closing out
// The first one that does so will return it's url
// These are blocking operations because they are unbuffered channels.
// This works though because `select` allows us to wait on multiple channels
case <-ping(a):
return a, nil
case <-ping(b):
return b, nil
// Super handy function during select, returns a channel, waits the timeout, then
//sends the current time that it was triggered at.
// Helps get us out of a blocking case.
case <-time.After(timeout):
return "", fmt.Errorf("timed out waiting for %s and %s", a, b)
}
}
// In our case, we don't care what type is sent to
//the channel, we just want to signal we are done
// and closing the channel works perfectly!
// a chan struct{} is the smallest data type available
// from a memory perspective
func ping(url string) chan struct{} {
// Notice how we have to use make when creating a channel; rather than say var ch chan struct{}. When you use var the variable will be initialised with the "zero" value of the type. So for string it is "", int it is 0, etc.
// For channels the zero value is nil and if you try and send to it with <- it will block forever because you cannot send to nil channels
ch := make(chan struct{})
go func() {
http.Get(url)
close(ch)
}()
return ch
}
// func measureResponseTime(url string) time.Duration {
// start := time.Now()
// http.Get(url)
// return time.Since(start)
// }

@ -0,0 +1,51 @@
package my_select
import (
"net/http"
"net/http/httptest"
"testing"
"time"
)
func TestRacer(t *testing.T) {
t.Run("compares speeds of servers, returning url of the fastest one", func(t *testing.T) {
slowServer := makeDelayedServer(20 * time.Microsecond)
defer slowServer.Close()
fastServer := makeDelayedServer(0 * time.Microsecond)
defer fastServer.Close()
slowUrl := slowServer.URL
fastUrl := fastServer.URL
want := fastUrl
got, _ := Racer(slowUrl, fastUrl)
if got != want {
t.Errorf("got %q, want %q", got, want)
}
})
t.Run("returns an error if server doesn't respond within 10s", func(t *testing.T) {
serverA := makeDelayedServer(50 * time.Millisecond)
serverB := makeDelayedServer(60 * time.Millisecond)
defer serverA.Close()
defer serverB.Close()
_, err := ConfigurableRacer(serverA.URL, serverB.URL, 10*time.Millisecond)
if err == nil {
t.Error("expected an error but didn't get one")
}
})
}
func makeDelayedServer(delay time.Duration) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(delay)
w.WriteHeader(http.StatusOK)
}))
}

@ -0,0 +1,3 @@
module my_sync
go 1.23.1

@ -0,0 +1,23 @@
package my_sync
import "sync"
type Counter struct {
// mutual exclusion lock. Zero value is an unlocked Mutex
// If we embedded the type it would be c.Lock() but that also makes all other
// Mutex functions part of the public interface, which we don't want.
// sync.Mutex
mu sync.Mutex
count int
}
func (c *Counter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
func (c *Counter) Value() int {
return c.count
}

@ -0,0 +1,63 @@
package my_sync
// READ https://go.dev/wiki/MutexOrChannel. The gist is that go prefers to share data by
// communicating, not by sharing. This mean stylistically you will see a lot more chan than Mutex
// Channels are good for
// passing ownership of data,
// distributing units of work,
// communicating async results
// Mutexes are good for caches, state.
import (
"sync"
"testing"
)
// go vet
// # my_sync
// # [my_sync]
// ./my_sync_test.go:8:36: assertCount passes lock by value: my_sync.Counter contains sync.Mutex
// ./my_sync_test.go:22:18: call of assertCount copies lock value: my_sync.Counter contains sync.Mutex
// ./my_sync_test.go:42:18: call of assertCount copies lock value: my_sync.Counter contains sync.Mutex
// We don't want to copy a mutex. So this should be changed to take a pointer to a counter
func assertCount(t testing.TB, got *Counter, want int) {
t.Helper()
if got.Value() != want {
t.Errorf("got %d, want %d", got.Value(), want)
}
}
func TestCounter(t *testing.T) {
t.Run("Incrementing the counter 3 times leaves it at 3", func(t *testing.T) {
// We could also implemented a `func NewCounter() *Counter {}` if we wanted to.
counter := Counter{}
counter.Inc()
counter.Inc()
counter.Inc()
assertCount(t, &counter, 3)
})
t.Run("it runs safely concurrently", func(t *testing.T) {
wantedCount := 1000
counter := Counter{}
var wg sync.WaitGroup
wg.Add(wantedCount)
for i := 0; i < wantedCount; i++ {
go func() {
counter.Inc()
wg.Done()
}()
}
// blocking call to wait for all goroutines
wg.Wait()
assertCount(t, &counter, wantedCount)
})
}

@ -0,0 +1,3 @@
module reflection
go 1.23.1

@ -0,0 +1,99 @@
package reflection
import "reflect"
// Go allows for `interface{}` which we can think as as just
// any type. (any is just an alias for interface{}) this allows for
// some metaprogramming features in the language reflection being one
// of them. Reflection allows us to examine structure at runtime.
// The downside, you lose type safety and performance impact.
// Only use reflection if you really need to.
// See also https://blog.golang.org/laws-of-reflection
func walk(x interface{}, fn func(input string)) {
// This code is very unsafe and very naive,
val := getValue(x)
// if val.Kind() == reflect.Slice {
// for i := 0; i < val.Len(); i++ {
// walk(val.Index(i).Interface(), fn)
// }
// return
// }
// // This would panic if there were no fields
// field := val.Field(0)
// // This would be wrong if the field had any value other than a string
// fn(field.String())
// // change for test table ( will fail case 2)
// for i := 0; i < val.NumField(); i++ {
// field := val.Field(i)
// fn(field.String())
// }
// THIS GOT messy
// for i := 0; i < val.NumField(); i++ {
// field := val.Field(i)
// // if field.Kind() == reflect.String {
// // fn(field.String())
// // }
// // if field.Kind() == reflect.Struct {
// // // it got recursive
// // walk(field.Interface(), fn)
// // }
// switch field.Kind() {
// case reflect.String:
// fn(field.String())
// case reflect.Struct:
// walk(field.Interface(), fn)
// }
// }
switch val.Kind() {
case reflect.Struct:
for i := 0; i < val.NumField(); i++ {
walk(val.Field(i).Interface(), fn)
}
case reflect.Slice, reflect.Array:
for i := 0; i < val.Len(); i++ {
walk(val.Index(i).Interface(), fn)
}
case reflect.Map:
for _, key := range val.MapKeys() {
walk(val.MapIndex(key).Interface(), fn)
}
case reflect.Chan:
for {
if v, ok := val.Recv(); ok {
walk(v.Interface(), fn)
} else {
break
}
}
case reflect.Func:
valFnResult := val.Call(nil)
for _, res := range valFnResult {
walk(res.Interface(), fn)
}
case reflect.String:
fn(val.String())
}
}
func getValue(x interface{}) reflect.Value {
val := reflect.ValueOf(x)
if val.Kind() == reflect.Pointer {
val = val.Elem()
}
return val
}

@ -0,0 +1,177 @@
package reflection
import (
"reflect"
"testing"
)
type Person struct {
Name string
Profile Profile
}
type Profile struct {
Age int
City string
}
func assertContains(t testing.TB, haystack []string, needle string) {
t.Helper()
contains := false
for _, x := range haystack {
if x == needle {
contains = true
}
}
if !contains {
t.Errorf("expected %v to contain %q but it didn't", haystack, needle)
}
}
func TestWalk(t *testing.T) {
cases := []struct {
Name string
Input interface{}
ExpectedCalls []string
}{
// case 0
{
Name: "struct with on string field",
Input: struct{ Name string }{"Drew"},
ExpectedCalls: []string{"Drew"},
},
// case 1
{
"struct with two string fields",
struct {
Name string
City string
}{"Chris", "London"},
[]string{"Chris", "London"},
},
// case 2
{
"struct with non string field",
struct {
Name string
Age int
}{"Chris", 33},
[]string{"Chris"},
},
// case 3 Nested Struct
{
"nested fields",
Person{
"Chris",
Profile{33, "London"},
},
[]string{"Chris", "London"},
},
// case 4 struct as pointer
{
"pointers to things",
&Person{
"Chris",
Profile{33, "London"},
},
[]string{"Chris", "London"},
},
// case 5 slice of profiles
{
"slices",
[]Profile{
{33, "London"},
{34, "Reykjavík"},
},
[]string{"London", "Reykjavík"},
},
// case 6 array
{
"arrays",
[2]Profile{
{33, "London"},
{34, "Reykjavík"},
},
[]string{"London", "Reykjavík"},
},
// case 7 map THIS HAS A GOTCHA
// maps in go do not guarantee order. It will eventually fake
// moved to a separate test run to handle that case
// {
// "maps",
// map[string]string{
// "Cow": "Moo",
// "Sheep": "Baa",
// },
// []string{"Moo", "Baa"},
// },
}
for _, test := range cases {
t.Run(test.Name, func(t *testing.T) {
var got []string
walk(test.Input, func(input string) {
got = append(got, input)
})
if !reflect.DeepEqual(got, test.ExpectedCalls) {
t.Errorf("get %v, want %v", got, test.ExpectedCalls)
}
})
}
t.Run("with mas", func(t *testing.T) {
aMap := map[string]string{
"Cow": "Moo",
"Sheep": "Baa",
}
var got []string
walk(aMap, func(input string) {
got = append(got, input)
})
assertContains(t, got, "Moo")
assertContains(t, got, "Baa")
})
t.Run("with channels", func(t *testing.T) {
aChannel := make(chan Profile)
go func() {
aChannel <- Profile{33, "Berlin"}
aChannel <- Profile{34, "Katowice"}
close(aChannel)
}()
var got []string
want := []string{"Berlin", "Katowice"}
walk(aChannel, func(input string) {
got = append(got, input)
})
if !reflect.DeepEqual(got, want) {
t.Errorf("got %v, want %v", got, want)
}
})
t.Run("with function", func(t *testing.T) {
aFunction := func() (Profile, Profile) {
return Profile{33, "Berlin"}, Profile{34, "Katowice"}
}
var got []string
want := []string{"Berlin", "Katowice"}
walk(aFunction, func(input string) {
got = append(got, input)
})
if !reflect.DeepEqual(got, want) {
t.Errorf("got %v, want %v", got, want)
}
})
}
Loading…
Cancel
Save