davideisinger.com

My personal website
Log | Files | Refs | README

crawshaw-io-k5slfj.txt (24360B)


      1 One process programming notes (with Go and SQLite)
      2 
      3 2018 July 30
      4 
      5 Blog-ified version of a talk I gave at [1]Go Northwest.
      6 
      7 This content covers my recent exploration of writing internet services, iOS
      8 apps, and macOS programs as an indie developer.
      9 
     10 There are several topics here that should each have their own blog post. But as
     11 I have a lot of programming to do I am going to put these notes up as is and
     12 split the material out some time later.
     13 
     14 My focus has been on how to adapt the lessons I have learned working in teams
     15 at Google to a single programmer building small business work. There are many
     16 great engineering practices in Silicon Valley's big companies and
     17 well-capitalized VC firms, but one person does not have enough bandwidth to use
     18 them all and write software. The exercise for me is: what to keep and what must
     19 go.
     20 
     21 If I have been doing it right, the technology and techniques described here
     22 will sound easy. I have to fit it all in my head while having enough capacity
     23 left over to write software people want. Every extra thing has great cost,
     24 especially rarely touched software that comes back to bite in the middle of the
     25 night six months later.
     26 
     27 Two key technologies I have decided to use are Go and SQLite.
     28 
     29 A brief introduction to SQLite
     30 
     31 SQLite is an implementation of SQL. Unlike traditional database implementations
     32 like PostgreSQL or MySQL, SQLite is a self-contained C library designed to be
     33 embedded into programs. It has been built by D. Richard Hipp since its release
     34 in 2000, and in the past 18 years other open source contributors have helped.
     35 At this point it has been around most of the time I have been programming and
     36 is a core part of my programming toolbox.
     37 
     38 Hands-on with the SQLite command line tool
     39 
     40 Rather than talk through SQLite in the abstract, let me show it to you.
     41 
     42 A kind person on Kaggle has [2]provided a CSV file of the plays of Shakespeare.
     43 Let's build an SQLite database out of it.
     44 
     45 $ head shakespeare_data.csv
     46 "Dataline","Play","PlayerLinenumber","ActSceneLine","Player","PlayerLine"
     47 "1","Henry IV",,,,"ACT I"
     48 "2","Henry IV",,,,"SCENE I. London. The palace."
     49 "3","Henry IV",,,,"Enter KING HENRY, LORD JOHN OF LANCASTER, the EARL of WESTMORELAND, SIR WALTER BLUNT, and others"
     50 "4","Henry IV","1","1.1.1","KING HENRY IV","So shaken as we are, so wan with care,"
     51 "5","Henry IV","1","1.1.2","KING HENRY IV","Find we a time for frighted peace to pant,"
     52 "6","Henry IV","1","1.1.3","KING HENRY IV","And breathe short-winded accents of new broils"
     53 "7","Henry IV","1","1.1.4","KING HENRY IV","To be commenced in strands afar remote."
     54 "8","Henry IV","1","1.1.5","KING HENRY IV","No more the thirsty entrance of this soil"
     55 "9","Henry IV","1","1.1.6","KING HENRY IV","Shall daub her lips with her own children's blood,"
     56 
     57 First, let's use the sqlite command line tool to create a new database and
     58 import the CSV.
     59 
     60 $ sqlite3 shakespeare.db
     61 sqlite> .mode csv
     62 sqlite> .import shakespeare_data.csv import
     63 
     64 Done! A couple of SELECTs will let us quickly see if it worked.
     65 
     66 sqlite> SELECT count(*) FROM import;
     67 111396
     68 sqlite> SELECT * FROM import LIMIT 10;
     69 1,"Henry IV","","","","ACT I"
     70 2,"Henry IV","","","","SCENE I. London. The palace."
     71 3,"Henry IV","","","","Enter KING HENRY, LORD JOHN OF LANCASTER, the EARL of WESTMORELAND, SIR WALTER BLUNT, and others"
     72 4,"Henry IV",1,1.1.1,"KING HENRY IV","So shaken as we are, so wan with care,"
     73 5,"Henry IV",1,1.1.2,"KING HENRY IV","Find we a time for frighted peace to pant,"
     74 6,"Henry IV",1,1.1.3,"KING HENRY IV","And breathe short-winded accents of new broils"
     75 7,"Henry IV",1,1.1.4,"KING HENRY IV","To be commenced in strands afar remote."
     76 8,"Henry IV",1,1.1.5,"KING HENRY IV","No more the thirsty entrance of this soil"
     77 9,"Henry IV",1,1.1.6,"KING HENRY IV","Shall daub her lips with her own children's blood,"
     78 
     79 Looks good! Now we can do a little cleanup. The original CSV contains a column
     80 called AceSceneLine that uses dots to encode Act number, Scene number, and Line
     81 number. Those would look much nicer as their own columns.
     82 
     83 sqlite> CREATE TABLE plays (rowid INTEGER PRIMARY KEY, play, linenumber, act, scene, line, player, text);
     84 sqlite> .schema
     85 CREATE TABLE import (rowid primary key, play, playerlinenumber, actsceneline, player, playerline);
     86 CREATE TABLE plays (rowid primary key, play, linenumber, act, scene, line, player, text);
     87 sqlite> INSERT INTO plays SELECT
     88         row AS rowid,
     89         play,
     90         playerlinenumber AS linenumber,
     91         substr(actsceneline, 1, 1) AS act,
     92         substr(actsceneline, 3, 1) AS scene,
     93         substr(actsceneline, 5, 5) AS line,
     94         player,
     95         playerline AS text
     96         FROM import;
     97 
     98 (The substr above can be improved by using instr to find the '.' characters.
     99 Exercise left for the reader.)
    100 
    101 Here we used the INSERT ... SELECT syntax to build a table out of another
    102 table. The ActSceneLine column was split apart using the builtin SQLite
    103 function substr, which slices strings.
    104 
    105 The result:
    106 
    107 sqlite> SELECT * FROM plays LIMIT 10;
    108 1,"Henry IV","","","","","","ACT I"
    109 2,"Henry IV","","","","","","SCENE I. London. The palace."
    110 3,"Henry IV","","","","","","Enter KING HENRY, LORD JOHN OF LANCASTER, the EARL of WESTMORELAND, SIR WALTER BLUNT, and others"
    111 4,"Henry IV",1,1,1,1,"KING HENRY IV","So shaken as we are, so wan with care,"
    112 5,"Henry IV",1,1,1,2,"KING HENRY IV","Find we a time for frighted peace to pant,"
    113 6,"Henry IV",1,1,1,3,"KING HENRY IV","And breathe short-winded accents of new broils"
    114 7,"Henry IV",1,1,1,4,"KING HENRY IV","To be commenced in strands afar remote."
    115 8,"Henry IV",1,1,1,5,"KING HENRY IV","No more the thirsty entrance of this soil"
    116 9,"Henry IV",1,1,1,6,"KING HENRY IV","Shall daub her lips with her own children's blood,"
    117 
    118 Now we have our data, let us search for something:
    119 
    120 sqlite> SELECT * FROM plays WHERE text LIKE "whether tis nobler%";
    121 sqlite>
    122 
    123 That did not work. Hamlet definitely says that, but perhaps the text formatting
    124 is slightly off. SQLite to the rescue. It ships with a Full Text Search
    125 extension compiled in. Let us index all of Shakespeare with FTS5:
    126 
    127 sqlite> CREATE VIRTUAL TABLE playsearch USING fts5(playsrowid, text);
    128 sqlite> INSERT INTO playsearch SELECT rowid, text FROM plays;
    129 
    130 Now we can search for our soliloquy:
    131 
    132 sqlite> SELECT rowid, text FROM playsearch WHERE text MATCH "whether tis nobler";
    133 34232|Whether 'tis nobler in the mind to suffer
    134 
    135 Success! The act and scene can be acquired by joining with our original table.
    136 
    137 sqlite> SELECT play, act, scene, line, player, plays.text
    138         FROM playsearch
    139         INNER JOIN plays ON playsearch.playsrowid = plays.rowid
    140         WHERE playsearch.text MATCH "whether tis nobler";
    141 Hamlet|3|1|65|HAMLET|Whether 'tis nobler in the mind to suffer
    142 
    143 Let's clean up.
    144 
    145 sqlite> DROP TABLE import;
    146 sqlite> VACUUM;
    147 
    148 Finally, what does all of this look like on the file system?
    149 
    150 $ ls -l
    151 -rwxr-xr-x@ 1 crawshaw  staff  10188854 Apr 27  2017 shakespeare_data.csv
    152 -rw-r--r--  1 crawshaw  staff  22286336 Jul 25 22:05 shakespeare.db
    153 
    154 There you have it. The SQLite database contains two full copies of the plays of
    155 Shakespeare, one with a full text search index, and stores both of them in
    156 about twice the space it takes the original CSV file to store one. Not bad.
    157 
    158 That should give you a feel for the i-t-e of SQLite.
    159 
    160 And scene.
    161 
    162 Using SQLite from Go
    163 
    164 The standard database/sql
    165 
    166 There are a number of cgo-based [3]database/sql drivers available for SQLite.
    167 The most popular one appears to be [4]github.com/mattn/go-sqlite3. It gets the
    168 job done and is probably what you want.
    169 
    170 Using the database/sql package it is straightforward to open an SQLite database
    171 and execute SQL statements on it. For example, we can run the FTS query from
    172 earlier using this Go code:
    173 
    174 package main
    175 
    176 import (
    177         "database/sql"
    178         "fmt"
    179         "log"
    180 
    181         _ "github.com/mattn/go-sqlite3"
    182 )
    183 
    184 func main() {
    185         db, err := sql.Open("sqlite3", "shakespeare.db")
    186         if err != nil {
    187                 log.Fatal(err)
    188         }
    189         defer db.Close()
    190         stmt, err := db.Prepare(`
    191                 SELECT play, act, scene, plays.text
    192                 FROM playsearch
    193                 INNER JOIN plays ON playsearch.playrowid = plays.rowid
    194                 WHERE playsearch.text MATCH ?;`)
    195         if err != nil {
    196                 log.Fatal(err)
    197         }
    198         var play, text string
    199         var act, scene int
    200         err = stmt.QueryRow("whether tis nobler").Scan(&play, &act, &scene, &text)
    201         if err != nil {
    202                 log.Fatal(err)
    203         }
    204         fmt.Printf("%s %d:%d: %q\n", play, act, scene, text)
    205 }
    206 
    207 Executing it yields:
    208 
    209 Hamlet 3:1 "Whether 'tis nobler in the mind to suffer"
    210 
    211 A low-level wrapper: crawshaw.io/sqlite
    212 
    213 Just as SQLite steps beyond the basics of SELECT, INSERT, UPDATE, DELETE with
    214 full-text search, it has several other interesting features and extensions that
    215 cannot be accessed by SQL statements alone. These need specialized interfaces,
    216 and many of the interfaces are not supported by any of the existing drivers.
    217 
    218 So I wrote my own. You can get it from [5]crawshaw.io/sqlite. In particular, it
    219 supports the streaming blob interface, the [6]session extension, and implements
    220 the necessary sqlite_unlock_notify machinery to make good use of the [7]shared
    221 cache for connection pools. I am going to cover these features through two use
    222 case studies: the client and the cloud.
    223 
    224 cgo
    225 
    226 All of these approaches rely on cgo for integrating C into Go. This is
    227 straightforward to do, but adds some operational complexity. Building a Go
    228 program using SQLite requires a C compiler for the target.
    229 
    230 In practice, this means if you develop on macOS you need to install a
    231 cross-compiler for linux.
    232 
    233 Typical concerns about the impact on software quality of adding C code to Go do
    234 not apply to SQLite as it has an extraordinary degree of testing. The quality
    235 of the code is exceptional.
    236 
    237 Go and SQLite for the client
    238 
    239 I am building an [8]iOS app, with almost all the code written in Go and the UI
    240 provided by a web view. This app has a full copy of the user data, it is not a
    241 thin view onto an internet server. This means storing a large amount of local,
    242 structured data, on-device full text searching, background tasks working on the
    243 database in a way that does not disrupt the UI, and syncing DB changes to a
    244 backup in the cloud.
    245 
    246 That is a lot of moving parts for a client. More than I want to write in
    247 JavaScript, and more than I want to write in Swift and then have to promptly
    248 rewrite if I ever manage to build an Android app. More importantly, the server
    249 is in Go, and I am one independent developer. It is absolutely vital I reduce
    250 the number of moving pieces in my development environment to the smallest
    251 possible number. Hence the effort to build (the big bits) of a client using the
    252 exact same technology as my server.
    253 
    254 The Session extension
    255 
    256 The session extension lets you start a session on an SQLite connection. All
    257 changes made to the database through that connection are bundled into a
    258 patchset blob. The extension also provides method for applying the generated
    259 patchset to a table.
    260 
    261 func (conn *Conn) CreateSession(db string) (*Session, error)
    262 
    263 func (s *Session) Changeset(w io.Writer) error
    264 
    265 func (conn *Conn) ChangesetApply(
    266         r          io.Reader,
    267         filterFn   func(tableName string) bool,
    268         conflictFn func(ConflictType, ChangesetIter) ConflictAction,
    269 ) error
    270 
    271 This can be used to build a very simple client-sync system. Collect the changes
    272 made in a client, periodically bundle them up into a changeset and upload it to
    273 the server where it is applied to a backup copy of the database. If another
    274 client changes the database then the server advertises it to the client, who
    275 downloads a changeset and applies it.
    276 
    277 This requires a bit of care in the database design. The reason I kept the FTS
    278 table separate in the Shakespeare example is I keep my FTS tables in a separate
    279 attached database (which in SQLite, means a different file). The cloud backup
    280 database never generates the FTS tables, the client is free to generate the
    281 tables in a background thread and they can lag behind data backups.
    282 
    283 Another point of care is minimizing conflicts. The biggest one is AUTOINCREMENT
    284 keys. By default the primary key of a rowid table is incremented, which means
    285 if you have multiple clients generating rowids you will see lots of conflicts.
    286 
    287 I have been trialing two different solutions. The first is having each client
    288 register a rowid range with the server and only allocate from its own range. It
    289 works. The second is randomly generating int64 values, and relying on the low
    290 collision rate. So far it works too. Both strategies have risks, and I haven't
    291 decided which is better.
    292 
    293 In practice, I have found I have to limit DB updates to a single connection to
    294 keep changeset quality high. (A changeset does not see changes made on other
    295 connections.) To do this I maintain a read-only pool of connections and a
    296 single guarded read-write connection in a pool of 1. The code only grabs the
    297 read-write connection when it needs it, and the read-only connections are
    298 enforced by the read-only bit on the SQLite connection.
    299 
    300 Nested Transactions
    301 
    302 The database/sql driver encourages the use of SQL transactions with its Tx
    303 type, but this does not appear to play well with nested transactions. This is a
    304 concept implemented by SAVEPOINT / RELEASE in SQL, and it makes for
    305 surprisingly composable code.
    306 
    307 If a function needs to make multiple statements in a transaction, it can open
    308 with a SAVEPOINT, then defer a call to RELEASE if the function produces no Go
    309 return error, or if it does instead call ROLLBACK and return the error.
    310 
    311 func f(conn *sqlite.Conn) (err error) {
    312         conn...SAVEPOINT
    313         defer func() {
    314                 if err == nil {
    315                         conn...RELEASE
    316                 } else {
    317                         conn...ROLLBACK
    318                 }
    319         }()
    320 }
    321 
    322 Now if this transactional function f needs to call another transactional
    323 function g, then g can use exactly the same strategy and f can call it in a
    324 very traditional Go way:
    325 
    326 if err := g(conn); err != nil {
    327         return err // all changes in f will be rolled back by the defer
    328 }
    329 
    330 The function g is also perfectly safe to use in its own right, as it has its
    331 own transaction.
    332 
    333 I have been using this SAVEPOINT + defer RELEASE or return an error semantics
    334 for several months now and find it invaluable. It makes it easy to safely wrap
    335 code in SQL transactions.
    336 
    337 The example above however is a bit bulky, and there are some edge cases that
    338 need to be handled. (For example, if the RELEASE fails, then an error needs to
    339 be returned.) So I have wrapped this up in a utility:
    340 
    341 func f(conn *sqlite.Conn) (err error) {
    342         defer sqlitex.Save(conn)(&err)
    343 
    344         // Code is transactional and can be stacked
    345         // with other functions that call sqlitex.Save.
    346 }
    347 
    348 The first time you see sqlitex.Save in action it can be a little off-putting,
    349 at least it was for me when I first created it. But I quickly got used to it,
    350 and it does a lot of heavy lifting. The first call to sqlitex.Save opens a
    351 SAVEPOINT on the conn and returns a closure that either RELEASEs or ROLLBACKs
    352 depending on the value of err, and sets err if necessary.
    353 
    354 Go and SQLite in the cloud
    355 
    356 I have spent several months now redesigning services I have encountered before
    357 and designing services for problems I would like to work on going forward. The
    358 process has led me to a general design that works for many problems and I quite
    359 enjoy building.
    360 
    361 It can be summarized as 1 VM, 1 Zone, 1 process programming.
    362 
    363 If this sounds ridiculously simplistic to you, I think that's good! It is
    364 simple. It does not meet all sorts of requirements that we would like our
    365 modern fancy cloud services to meet. It is not "serverless", which means when a
    366 service is extremely small it does not run for free, and when a service grows
    367 it does not automatically scale. Indeed, there is an explicit scaling limit.
    368 Right now the best server you can get from Amazon is roughly:
    369 
    370   • 128 CPU threads at ~4GHz
    371   • 4TB RAM
    372   • 25 Gbit ethernet
    373   • 10 Gbps NAS
    374   • hours of yearly downtime
    375 
    376 That is a huge potential downside of of one process programming. However, I
    377 claim that is a livable limit.
    378 
    379 I claim typical services do not hit this scaling limit.
    380 
    381 If you are building a small business, most products can grow and become
    382 profitable well under this limit for years. When you see the limit approaching
    383 in the next year or two, you have a business with revenue to hire more than one
    384 engineer, and the new team can, in the face of radically changing business
    385 requirements, rewrite the service.
    386 
    387 Reaching this limit is a good problem to have because when it comes you will
    388 have plenty of time to deal with it and the human resources you need to solve
    389 it well.
    390 
    391 Early in the life of a small business you don't, and every hour you spend
    392 trying to work beyond this scaling limit is an hour that would have been better
    393 spent talking to your customers about their needs.
    394 
    395 The principle at work here is:
    396 
    397 Don't use N computers when 1 will do.
    398 
    399 To go into a bit more technical detail,
    400 
    401 I run a single VM on AWS, in a single availability zone. The VM has three EBS
    402 volumes (this is Amazon name for NAS). The first holds the OS, logs, temporary
    403 files, and any ephemeral SQLite databases that are generated from the main
    404 databases, e.g. FTS tables. The second the primary SQLite database for the main
    405 service. The third holds the customer sync SQLite databases.
    406 
    407 The system is configured to periodically snapshot the system EBS volume and the
    408 customer EBS volumes to S3, the Amazon geo-redundant blob store. This is a
    409 relatively cheap operation that can be scripted, because only blocks that
    410 change are copied.
    411 
    412 The main EBS volume is backed up to S3 very regularly, by custom code that
    413 flushes the WAL cache. I'll explain that in a bit.
    414 
    415 The service is a single Go binary running on this VM. The machine has plenty of
    416 extra RAM that is used by linux's disk cache. (And that can be used by a second
    417 copy of the service spinning up for low down-time replacement.)
    418 
    419 The result of this is a service that has at most tens of hours of downtime a
    420 year, about as much change of suffering block loss as a physical computer with
    421 a RAID5 array, and active offsite backups being made every few minutes to a
    422 distributed system that is built and maintained by a large team.
    423 
    424 This system is astonishingly simple. I shell into one machine. It is a linux
    425 machine. I have a deploy script for the service that is ten lines long. Almost
    426 all of my performance work is done with pprof.
    427 
    428 On a medium sized VM I can clock 5-6 thousand concurrent requests with only a
    429 few hours of performance tuning. On the largest machine AWS has, tens of
    430 thousands.
    431 
    432 Now to talk a little more about the particulars of the stack:
    433 
    434 Shared cache and WAL
    435 
    436 To make the server extremely concurrent there are two important SQLite features
    437 I use. The first is the shared cache, which lets me allocate one large pool of
    438 memory to the database page cache and many concurrent connections can use it
    439 simultaneously. This requires some support in the driver for
    440 sqlite_unlock_notify so user code doesn't need to deal with locking events, but
    441 that is transparent to end user code.
    442 
    443 The second is the Write Ahead Log. This is a mode SQLite can be knocked into at
    444 the beginning of connection which changes the way it writes transactions to
    445 disk. Instead of locking the database and making modifications along with a
    446 rollback journal, it appends the new change to a separate file. This allows
    447 readers to work concurrently with the writer. The WAL has to be flushed
    448 periodically by SQLite, which involves locking the database and writing the
    449 changes from it. There are default settings for doing this.
    450 
    451 I override these and execute WAL flushes manually from a package that, when it
    452 is done, also triggers an S3 snapshot. This package is called reallyfsync, and
    453 if I can work out how to test it properly I will make it open source.
    454 
    455 Incremental Blob API
    456 
    457 Another smaller, but important to my particular server feature, is SQLite's [9]
    458 incremental blob API. This allows a field of bytes to be read and written in
    459 the DB without storing all the bytes in memory simultaneously, which matters
    460 when it is possible for each request to be working with hundreds of megabytes,
    461 but you want tens of thousands of potential concurrent requests.
    462 
    463 This is one of the places where the driver deviates from being a close-to-cgo
    464 wrapper to be more [10]Go-like:
    465 
    466 type Blob
    467     func (blob *Blob) Close() error
    468     func (blob *Blob) Read(p []byte) (n int, err error)
    469     func (blob *Blob) ReadAt(p []byte, off int64) (n int, err error)
    470     func (blob *Blob) Seek(offset int64, whence int) (int64, error)
    471     func (blob *Blob) Size() int64
    472     func (blob *Blob) Write(p []byte) (n int, err error)
    473     func (blob *Blob) WriteAt(p []byte, off int64) (n int, err error)
    474 
    475 This looks a lot like a file, and indeed can be used like a file, with one
    476 caveat: the size of a blob is set when it is created. (As such, I still find
    477 temporary files to be useful.)
    478 
    479 Designing with one process programming
    480 
    481 I start with: Do you really need N computers?
    482 
    483 Some problems really do. For example, you cannot build a low-latency index of
    484 the public internet with only 4TB of RAM. You need a lot more. These problems
    485 are great fun, and we like to talk a lot about them, but they are a relatively
    486 small amount of all the code written. So far all the projects I have been
    487 developing post-Google fit on 1 computer.
    488 
    489 There are also more common sub-problems that are hard to solve with one
    490 computer. If you have a global customer base and need low-latency to your
    491 server, the speed of light gets in the way. But many of these problems can be
    492 solved with relatively straightforward CDN products.
    493 
    494 Another great solution to the speed of light is geo-sharding. Have complete and
    495 independent copies of your service in multiple datacenters, move your user's
    496 data to the service near them. This can be as easy as having one small global
    497 redirect database (maybe SQLite on geo-redundant NFS!) redirecting the user to
    498 a specific DNS name like {us-east, us-west}.mservice.com.
    499 
    500 Most problems do fit in one computer, up to a point. Spend some time
    501 determining where that point is. If it is years away there is a good chance one
    502 computer will do.
    503 
    504 Indie dev techniques for the corporate programmer
    505 
    506 Even if you do not write code in this particular technology stack and you are
    507 not an independent developer, there is value here. Use the one big VM, one
    508 zone, one process Go, SQLite, and snapshot backup stack as a hypothetical tool
    509 to test your designs.
    510 
    511 So add a hypothetical step to your design process: If you solved your problem
    512 on this stack with one computers, how far could you get? How many customers
    513 could you support? At what size would you need to rewrite your software?
    514 
    515 If this indie mini stack would last your business years, you might want to
    516 consider delaying the adoption of modern cloud software.
    517 
    518 If you are a programmer at a well-capitalized company, you may also want to
    519 consider what development looks like for small internal or experimental
    520 projects. Do your coworkers have to use large complex distributed systems for
    521 policy reasons? Many of these projects will never need to scale beyond one
    522 computer, or if they do they will need a rewrite to deal with shifting
    523 requirements. In which case, find a way to make an indie stack, linux VMs with
    524 a file system, available for prototyping and experimentation.
    525 
    526 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    527 [11]Index
    528 [12]github.com/crawshaw
    529 [13]twitter.com/davidcrawshaw
    530 [email protected]
    531 
    532 References:
    533 
    534 [1] https://gonorthwest.io/
    535 [2] https://www.kaggle.com/kingburrito666/shakespeare-plays
    536 [3] https://golang.org/pkg/database/sql
    537 [4] https://github.com/mattn/go-sqlite3
    538 [5] https://crawshaw.io/sqlite
    539 [6] https://www.sqlite.org/sessionintro.html
    540 [7] https://www.sqlite.org/sharedcache.html
    541 [8] https://www.posticulous.com/
    542 [9] https://www.sqlite.org/c3ref/blob_open.html
    543 [10] https://godoc.org/crawshaw.io/sqlite#Blob
    544 [11] https://crawshaw.io/
    545 [12] https://github.com/crawshaw
    546 [13] https://twitter.com/davidcrawshaw