├── LICENSE ├── README.md ├── bgr.png ├── bolt.png ├── main.slide └── react.png /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Ben Johnson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | The Burger Stack: Simplifying Web Application Development 2 | ========================================================= 3 | 4 | ### Target Audience 5 | 6 | Intermediate developers looking to simplify development. 7 | 8 | 9 | ### Introduction (5 sentences) 10 | 11 | Gophers love to talk about how great Go is. And it is! I've used it to write 12 | databases and distributed systems and command line tools. It's my favorite 13 | language. But ever since I started writing Go there's been one area that 14 | everyone advises against using Go: web development. And that's terrible. Web 15 | applications are an enormous part of our industry! 16 | 17 | Additionally, web development hasn't seen limited improvement over the years. 18 | We started with the LAMP stack 20 years ago where we used slow languages like 19 | PHP or Perl to connect to a difficult to configure MySQL server. 10 years later 20 | we saw Rails where we used Ruby, a slow language, to connect to complex SQL 21 | databases. Recently we've see the MEAN stack where we write JavaScript on the 22 | backend to connect to MongoDB. It's almost like we've gone backwards in our 23 | industry. 24 | 25 | However, all is not lost. If we consider some recent developments in our 26 | industry over the past few years, Go begins to stand out as a serious contender 27 | within the web application space. 28 | 29 | 30 | ### Single Page Applications 31 | 32 | One big flaw with Go is that it lacks a strong templating language. There is 33 | the `template` package but it's difficult to use at scale for a large 34 | application. However, in recent years there has been enormous adoption for 35 | Single Page Applications (SPAs). We've seen several technologies come along such 36 | as AngularJS, Backbone.js, & Ember.js but none of these have seen the crazy 37 | adoption that Facebook's React has. 38 | 39 | React does a lot of awesome UI stuff and if this was a JavaScript conference 40 | then I'd talk about it more. However, it's important to note that React renders 41 | client side so it only needs to consume an API. No templating is needed on the 42 | server side. 43 | 44 | 45 | ### SQL Alternatives 46 | 47 | Another flaw that Go has is that working with SQL databases is not much fun. 48 | Rails has a pretty ORM built-in but you still need to know enough to optimize 49 | things like N+1 queries. You also need to configure and maintain your database 50 | server. Honestly, it feels like we adopted relational databases 40 years ago 51 | as a standard and we've been doing whatever we can to avoid SQL ever since. 52 | 53 | However, there is a much better way to avoid SQL, relational databases, and set 54 | theory. Simply don't use them. Go excels at working with byte slices so instead 55 | of mapping objects to relations, you can map your objects to bytes. 56 | 57 | Now if you think that mapping to bytes is complex, don't worry, this is a 58 | problem that computer scientists have devoted years of research to and it's 59 | called "serialization". There are so many serialization libraries to choose 60 | from: Protocol Buffers, FlatBuffers, Avro, MessagePack, JSON. This serialization 61 | is essentially the schema for your data. 62 | 63 | Once we're mapping to bytes, we don't need the high level features of most 64 | databases. We can get rid of SQL. Since we have no SQL we can get rid of the 65 | query planner and query cache. Let's go one step further and get rid of the 66 | server altogether and use an embedded database. At this point we have the 67 | simplest database possible: an embedded key/value store. 68 | 69 | There are several key/value stores available but we want something in pure Go, 70 | strong transactional support, and read optimized so we're going to talk about 71 | the most amazing database ever created: BoltDB. Disclaimer: I'm the author of 72 | BoltDB so I'm a little biased. 73 | 74 | Bolt, Go, React: the BGR stack. That's all we need to build an extremely fast, 75 | easy to develop, easy to deploy web application. Many of you are probably 76 | familiar with builing APIs with Go and I'm not going to give a React talk at a 77 | Go conference so let's look how you use Bolt for everyone's favorite web app 78 | construct: CRUD. 79 | 80 | 81 | ### Domain Types & Serialization 82 | 83 | The first thing you need in any web app is a way to manage users so let's build 84 | a `UserStore`. We'll keep it simple and our user will have two fields: an ID 85 | and a username. 86 | 87 | We'll need to serialize this object too so let's use Protocol Buffers to do 88 | that. I like protobufs because it's really fast and it has versioning built-in. 89 | Here's our protobuf definition for our `User` type. Now we can add a 90 | "generate" line in our source and it'll automatically regenerate whenever we 91 | call `go generate`. I also like to put this generated code in my `internal` 92 | package so it's hidden from the rest of my application and godoc. 93 | 94 | Next, we'll implement the little known `BinaryMarshaler` and `BinaryUnmarshaler` 95 | interfaces. To marshal, we populate our protobuf object and marshal it. To 96 | unmarshal, we unmarshal our protobuf object and copy from it. 97 | 98 | 99 | ### Opening & initializing 100 | 101 | Bolt databases are extremely simple. It's just a single file. You can open it 102 | using `bolt.Open()` similar to how `os.Open()` works. Once our database is open, 103 | we'll need a place to store our user data and Bolt has something called a 104 | `Bucket` for this. A bucket is essentially a persisted map that maps unique 105 | byte slice keys to byte slice values. I use buckets analogously to tables in a 106 | relational database. 107 | 108 | We can ensure that we have a "Users" bucket created by starting a transaction 109 | and calling `CreateBucketIfNotExists()`. 110 | 111 | Transactions are an important part of Bolt. There are two kinds: read-only and 112 | read-write. They are full ACID transactions with serializable isolation. That 113 | means that they will have a snapshot view of the data from when the transaction 114 | started. You can have an unlimited number of simultaneous read-only transactions 115 | but only one read-write transaction can operate at a time. 116 | 117 | We start a transaction using `Begin()` and the only parameter is whether the 118 | transaction is writable. Make sure you ALWAYS defer rollback on your 119 | transactions! Leaving a transaction open in the event of an early return or a 120 | recovered panic will cause problems. 121 | 122 | 123 | ### Creating a user 124 | 125 | Let's add a new user. Let's walk through the code to see how this is done. 126 | First we start a read-write transaction. Next we'll grab our "Users" bucket. 127 | One each bucket there exists an autoincrementing sequence similar. We can grab 128 | a new ID for our user from there. 129 | 130 | Since we've already implemented `BinaryMarshaler`, we can easily convert our 131 | user to bytes. Then we'll map our ID to our encoded bytes using `Put()`. 132 | Finally, we'll commit the transaction and return an error if it failed for any 133 | reason. Again, these are true ACID transactions so any operations that occurred 134 | will be rolled back if anything fails. 135 | 136 | You'll notice in the `Put()` line I use a helper function called `itob()`. This 137 | is short for integer to bytes. I use this a lot and it saves me 3 lines over 138 | and over again. It also ensures that you're always using big endian encoding. 139 | Endianness is simply how you encode an integer to bytes and we always use 140 | big endian in Bolt because it makes our integer keys sortable. 141 | 142 | 143 | ### Reading a user 144 | 145 | Once you have users in your system, you need to get them out. Let's walk through 146 | it. Again, we start a transaction. This time it is read-only. It's important to 147 | use read-only transactions for read-only operations so that it can scale. 148 | Read-write transactions can only operate one at a time. Next we'll pull out our 149 | bytes using the `Get()` method on our bucket. If there is no value then the 150 | key doesn't exist and we can return a `nil` user. But if we get a value then 151 | decode it and return it. The defer rollback will be called and close out the 152 | transaction automatically. 153 | 154 | If you want to read all our users you can use a cursor on your bucket. A cursor 155 | is simply a way to iterate over a bucket. We'll start the transaction the same 156 | as before, then grab a cursor, and then we can traverse the whole bucket in a 157 | simple `for` loop. This calls `First()` to move the cursor to the beginning and 158 | then calls `Next()` until there are no more keys. We'll unmarshal and append on 159 | our slice for each user and return. 160 | 161 | 162 | ### Updating a user 163 | 164 | We've covered the "C" and the "R" of CRUD, next is update. Update is a bit like 165 | read and create put together. This time we'll use a different method in Bolt for 166 | running transactions called `Update()`. This will run a function in the context 167 | of a transaction. If the function returns an error, everything will rollback. 168 | Otherwise it will commit. This can be really helpful if our operation only 169 | returns an error. 170 | 171 | We'll also group some of the operations in if/else statements so it's easier to 172 | see related operations. First we grab the bucket and the value, just like in our 173 | read operation. If it doesn't exist we'll return an error. Otherwise we'll 174 | unmarshal the object. Once we have our object we can update it's state. Now with 175 | the new state we'll marshal and overwrite our previous value. Finally, returning 176 | `nil` at the end will cause our transaction to commit. 177 | 178 | 179 | ### Deleting a user. 180 | 181 | Finally we come to the "D" of CRUD. This operation is incredibly simple. We 182 | can use the `Update()` helper and simply delete the key from our bucket. 183 | 184 | And that's it! Full set of CRUD operations in a handful of lines of code. And 185 | the best part is that it's all Go. 186 | 187 | 188 | ### BoltDB in Practice 189 | 190 | There's a lot more to applications than just writing code. There's testing, 191 | deploying, and maintenance. Testing is quite simple. It's an embedded database 192 | and a single file so you can simply create a temp file for your test and delete 193 | it when the test is done. That also means that you can run your tests in 194 | parallel. 195 | 196 | When you deploy BoltDB, it's advised that you use SSDs. Bolt doesn't use 197 | compactions so there's a lot of random writes. Another gotcha is that Bolt 198 | returns byte slices that directly point to an mmap so these slices are only 199 | valid until you close a transaction. One huge benefit to this mmap, though, is 200 | that the data is in the OS page cache so it'll stay there even if you restart 201 | your application. You can use graceful restarts to have zero downtime. 202 | 203 | One big question about the maintenance side is how do you backup your data or 204 | handle catastrophic hardware failures? Bolt transactions implement the 205 | `io.WriterTo` interface so you can actually stream a full, consistent copy of 206 | your database to any writer. My personal favorite is adding an HTTP endpoint 207 | so that I can use `curl` as my backup tool. 208 | 209 | This does leave a window for data loss between snapshots. There is async 210 | streaming replication support coming in the future so that you can have a 211 | failover server and you can minimize that data loss window to only a few 212 | seconds. However, that is still in early stages. In the meantime you'll need 213 | to evaluate what window is acceptable. An SSD can copy out your database at 214 | 3 - 5 seconds per GB so it may be acceptable to take a snapshot hourly or even 215 | every 10 minutes depending on the size of your database. Sharding your database 216 | can also help keep it small. 217 | 218 | Performance is always a question that comes up with any database. Most people 219 | want the "fastest" database but it's difficult to evaluate speed without your 220 | specific workload. Also, most people don't need all the performance they think 221 | they do. Bolt is read optimized which is a good fit for most web applications. 222 | I generally give people these ballpark numbers: 2K random writes/sec, or up to 223 | 450,000 writes/sec if you can sort and batch your writes. For reads, it really 224 | depends on if your data is hot in memory. Many applications can fit their 225 | working set in memory -- especially with 226 | 227 | 228 | ### Conclusion (5 sentences) 229 | 230 | I believe it's time to move past the slow, complex stacks that permeate through 231 | our industry and adopt something simpler. I've shown how the BGR stack provides 232 | a simple, fast environment for not only developing applications but also 233 | deploying and scaling them. I'm excited to see the future of Go not just in the 234 | distributed systems and low-level services but also in the huge space of web 235 | development. Please, give the BGR stack a try on your next project! 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | ### Key Takeaways (bullet points) 244 | 245 | - LAMP & Rails provided great ORM and HTML templating tools 246 | - MEAN tried to modernize with MongoDB & JavaScript but who wants to write JS server side? 247 | - React has seen huge adoption so HTML templating is no longer needed 248 | - BoltDB avoids the headaches of SQL & remote servers so no ORM needed 249 | - Vertical scaling works for a long time 250 | - How to get started with BoltDB, sample CRUD operations 251 | - BoltDB in production 252 | 253 | 254 | -------------------------------------------------------------------------------- /bgr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benbjohnson/burger-stack/18000244f8b7e7a37991743e6926a8dedcff479f/bgr.png -------------------------------------------------------------------------------- /bolt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benbjohnson/burger-stack/18000244f8b7e7a37991743e6926a8dedcff479f/bolt.png -------------------------------------------------------------------------------- /main.slide: -------------------------------------------------------------------------------- 1 | The Burger Stack 2 | Simplifying Web Application Development 3 | 10 Jul 2016 4 | 5 | Ben Johnson 6 | benbjohnson@yahoo.com 7 | @benbjohnson 8 | 9 | 10 | * Go is awesome 🎉🎉🎉 11 | 12 | * Tons of use cases 13 | 14 | - Databases 15 | - Distributed systems 16 | - Low level networking 17 | - Infrastructure management 18 | - Command line tools 19 | 20 | But something is missing... 21 | 22 | * Web application development 23 | 24 | 25 | * Brief history of web application development 26 | 27 | LAMP (1990's) 28 | 29 | - Slow, interpreted language 30 | - Complex SQL database server 31 | 32 | Rails (2000's) 33 | 34 | - Slow, interpreted language 35 | - Hide complexity of SQL in ORM 36 | 37 | MEAN (2010's) 38 | 39 | - Faster, interpreted language 40 | - Ditch SQL entirely, just use untyped collections 41 | 42 | 43 | * Flaws of Go web development 44 | 45 | - Limited templating support 46 | - Limited ORM tooling 47 | 48 | 49 | * Rise of the SPA (Single Page Application) 50 | 51 | * Facebook React 52 | 53 | .image react.png 54 | 55 | * Facebook React 56 | 57 | - Client side rendering 58 | - Only needs an API 59 | 60 | 61 | * Working with SQL 62 | 63 | * Object/Relational Mapping (ORM) 64 | 65 | - Working with SQL in Go is a pain 66 | - Go lacks a standard ORM tool 67 | - ORMs are an elaborate way to avoid SQL 68 | 69 | * Best way to avoid SQL is to not use SQL! 70 | 71 | * Object to Byte Mapping 72 | 73 | - Go excels at working with bytes 74 | - Decades of research into object-to-byte mappings! 75 | 76 | * Serialization! 77 | 78 | Tons of options: 79 | 80 | - Protocol Buffers 81 | - FlatBuffers 82 | - Avro 83 | - MessagePack 84 | - JSON 85 | 86 | Works as a schema for your data. 87 | 88 | 89 | * stdlib: encoding package 90 | 91 | We need to implement marshaling: 92 | 93 | type BinaryMarshaler interface { 94 | MarshalBinary() (data []byte, err error) 95 | } 96 | 97 | type BinaryUnmarshaler interface { 98 | UnmarshalBinary(data []byte) error 99 | } 100 | 101 | .link https://golang.org/pkg/encoding/ 102 | 103 | 104 | * Simplify, simplify, simplify 105 | 106 | - Don't need SQL 107 | - Don't need queries 108 | - Don't need query planner 109 | - Don't need query cache 110 | - Don't even need a server 111 | 112 | 113 | * Use an embedded key/value store! 114 | 115 | * Database requirements for web application development 116 | 117 | - Pure Go 118 | - ACID transactions 119 | - Read optimized 120 | 121 | * BoltDB 122 | 123 | Greatest database platform ever to be created! 124 | 125 | .image bolt.png 126 | 127 | 128 | * BoltDB 129 | 130 | Disclaimer: I'm the author of BoltDB 131 | 132 | .image bolt.png 133 | 134 | 135 | * BGR Stack 136 | 137 | .image bgr.png 138 | 139 | Go gopher was designed by Renee French, React logo was designed at Facebook 140 | 141 | 142 | 143 | * Getting started with BoltDB 144 | 145 | * Installation 146 | 147 | Just type: 148 | 149 | $ go get github.com/boltdb/bolt 150 | 151 | And you're ready! 152 | 153 | 🎉🎉🎉 154 | 155 | 156 | * Cross platform support 157 | 158 | Works on: 159 | 160 | - Windows 161 | - Mac OS X 162 | - Linux 163 | - ARM 164 | - iOS 165 | - Android 166 | 167 | 168 | * CRUD 169 | 170 | * Domain Type 171 | 172 | We'll focus just on a simple data layer to perform CRUD operations on "users" 173 | 174 | // User represents a user in our system. 175 | type User struct { 176 | ID int 177 | Username string 178 | } 179 | 180 | 181 | * Protobuf definition 182 | 183 | `internal/internal.proto` 184 | 185 | package internal; 186 | 187 | message User { 188 | optional int64 ID = 1; 189 | optional string Username = 2; 190 | } 191 | 192 | - Field IDs provide versioning 193 | - No migration needed to add or remove fields 194 | 195 | 196 | * Go Generate 197 | 198 | Add this line to your main package: 199 | 200 | //go:generate protoc --gogo_out=. internal/internal.proto 201 | 202 | Automatically regenerate encoders/decoders: 203 | 204 | $ go generate 205 | 206 | 207 | 208 | * Ugly generated code 209 | 210 | package internal 211 | 212 | type User struct { 213 | ID *int64 `protobuf:"varint,1,opt,name=ID" json:"ID,omitempty"` 214 | Username *string `protobuf:"bytes,2,opt,name=Username" json:"Username,omitempty"` 215 | XXX_unrecognized []byte `json:"-"` 216 | } 217 | 218 | func (m *User) Reset() { *m = User{} } 219 | func (m *User) String() string { return proto.CompactTextString(m) } 220 | func (*User) ProtoMessage() {} 221 | func (*User) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{0} } 222 | 223 | func (m *User) GetID() int64 { 224 | if m != nil && m.ID != nil { 225 | return *m.ID 226 | } 227 | return 0 228 | } 229 | 230 | ... 231 | 232 | 233 | * BinaryMarshaler 234 | 235 | Encode a `*User` to a `[]byte`: 236 | 237 | // MarshalBinary encodes a user to binary format. 238 | func (u *User) MarshalBinary() ([]byte, error) { 239 | pb := internal.User{ 240 | ID: proto.Int64(int64(u.ID)), 241 | Username: proto.String(u.Username), 242 | } 243 | 244 | return proto.Marshal(&pb) 245 | } 246 | 247 | `*User` ➤ `*internal.User` ➤ `[]byte` 248 | 249 | 250 | * BinaryUnmarshaler 251 | 252 | Decode from a `[]byte` to a `*User`: 253 | 254 | func (u *User) UnmarshalBinary(data []byte) error { 255 | var pb internal.User 256 | if err := proto.Unmarshal(data, &pb); err != nil { 257 | return err 258 | } 259 | 260 | u.ID = int(pb.GetID()) 261 | u.Username = pb.GetUsername() 262 | 263 | return nil 264 | } 265 | 266 | `[]byte` ➤ `*internal.User` ➤ `*User` 267 | 268 | 269 | * Data Store 270 | 271 | * Data Store 272 | 273 | // Store represents the data storage layer. 274 | type Store struct { 275 | // Filepath to the data file. 276 | Path string 277 | 278 | db *bolt.DB 279 | } 280 | 281 | 282 | * Opening & initializing a Bolt database 283 | 284 | func (s *Store) Open() error { 285 | db, err := bolt.Open(path, 0666, nil) 286 | if err != nil { 287 | return err 288 | } 289 | s.db = db 290 | 291 | // Start a writable transaction. 292 | tx, err := s.db.Begin(true) 293 | if err != nil { 294 | return nil, err 295 | } 296 | defer tx.Rollback() 297 | 298 | // Initialize buckets to guarantee that they exist. 299 | tx.CreateBucketIfNotExists([]byte("Users")) 300 | 301 | // Commit the transaction. 302 | return tx.Commit() 303 | } 304 | 305 | 306 | * Creating a user 307 | 308 | * Creating a user 309 | 310 | func (s *Store) CreateUser(u *User) error { 311 | tx, err := s.db.Begin(true) 312 | if err != nil { 313 | return err 314 | } 315 | defer tx.Rollback() 316 | 317 | // Retrieve bucket and create new ID. 318 | bkt := tx.Bucket([]byte("Users")) 319 | seq, _ := bkt.NextSequence() 320 | u.ID = int(seq) 321 | 322 | // Marshal our user into bytes. 323 | buf, err := u.MarshalBinary() 324 | if err != nil { 325 | return err 326 | } 327 | 328 | // Save user to the bucket. 329 | if err := bkt.Put(itob(u.ID), buf); err != nil { 330 | return err 331 | } 332 | return tx.Commit() 333 | } 334 | 335 | * Helper function 336 | 337 | // itob encodes v as a big endian integer. 338 | func itob(v int) []byte { 339 | buf := make([]byte, 8) 340 | binary.BigEndian.PutUint64(buf, uint64(v)) 341 | return buf 342 | } 343 | 344 | 345 | * Retrieving a user 346 | 347 | * Retrieving a user 348 | 349 | func (s *Store) User(id int) (*User, error) { 350 | // Start a readable transaction. 351 | tx, err := s.db.Begin(false) 352 | if err != nil { 353 | return nil, err 354 | } 355 | defer tx.Rollback() 356 | 357 | // Read encoded user bytes. 358 | v := tx.Bucket([]byte("Users")).Get(itob(id)) 359 | if v == nil { 360 | return nil, nil 361 | } 362 | 363 | // Unmarshal bytes into a user. 364 | var u User 365 | if err := u.UnmarshalBinary(v); err != nil { 366 | return nil, err 367 | } 368 | 369 | return &u, nil 370 | } 371 | 372 | * Retrieving a list of users 373 | 374 | func (s *Store) Users() ([]*User, error) { 375 | // Start a readable transaction. 376 | tx, err := s.db.Begin(false) 377 | if err != nil { 378 | return nil, err 379 | } 380 | defer tx.Rollback() 381 | 382 | // Create a cursor on the user's bucket. 383 | c := tx.Bucket([]byte("Users")).Cursor() 384 | 385 | // Read all users into a slice. 386 | var a []*User 387 | for k, v := c.First(); k != nil; k, v = c.Next() { 388 | var u User 389 | if err := u.UnmarshalBinary(v); err != nil { 390 | return nil, err 391 | } 392 | a = append(a, &u) 393 | } 394 | 395 | return a, nil 396 | } 397 | 398 | * Iterating over buckets 399 | 400 | func (*bolt.Bucket) Cursor() *bolt.Cursor 401 | 402 | - Allows forward iteration: First(), Next() 403 | - Allows backward iteration: Last(), Prev() 404 | - Allows seeking to a key: Seek() 405 | - Always iterates keys in sorted order 406 | 407 | 408 | * Updating a user 409 | 410 | * Updating a user 411 | 412 | func (s *Store) SetUsername(id int, username string) error { 413 | return s.db.Update(func(tx *bolt.Tx) error { 414 | bkt := tx.Bucket([]byte("Users")) 415 | 416 | // Retrieve encoded user and decode. 417 | var u User 418 | if v := bkt.Get(itob(id)); v == nil { 419 | return ErrUserNotFound 420 | } else if err := u.UnmarshalBinary(v); err != nil { 421 | return err 422 | } 423 | 424 | // Update user. 425 | u.Username = username 426 | 427 | // Encode and save user. 428 | if buf, err := u.MarshalBinary(); err != nil { 429 | return err 430 | } else if err := bkt.Put(itob(id), buf); err != nil { 431 | return err 432 | } 433 | return nil 434 | }) 435 | } 436 | 437 | 438 | * Deleting a user 439 | 440 | * Deleting a user 441 | 442 | func (s *Store) DeleteUser(id int) error { 443 | return s.db.Update(func(tx *bolt.Tx) error { 444 | return tx.Bucket([]byte("Users")).Delete(itob(id)) 445 | }) 446 | } 447 | 448 | 449 | * BoltDB in Practice 450 | 451 | * In Development 452 | 453 | Awesomeness: 454 | 455 | - No dependencies 456 | - Pure Go 457 | - Stable API 458 | 459 | Gotchas: 460 | 461 | - Bolt returns byte slices pointing directly to a read-only mmap 462 | - Byte slices are only valid for the life of a transaction 463 | 464 | 465 | * In Testing 466 | 467 | Awesomeness: 468 | 469 | - Use `ioutil.TempFile()` 470 | - Easily parallelizable 471 | - No migrations, just start up 472 | 473 | 474 | * In Production 475 | 476 | Awesomeness: 477 | 478 | - Data is held in the OS page cache so it persists across application restarts 479 | 480 | Gotchas: 481 | 482 | - Requires a lot of random access so SSDs are advised 483 | 484 | 485 | * Backup & Restore 486 | 487 | - Strongly consistent backup 488 | - FUTURE: Async streaming replication coming 489 | 490 | 491 | * Backup & Restore 492 | 493 | `bolt.Tx` implements `io.WriterTo`: 494 | 495 | type WriterTo interface { 496 | WriteTo(w Writer) (n int64, err error) 497 | } 498 | 499 | - 3-5 sec/GB typically 500 | 501 | * HTTP Backup 502 | 503 | func (*Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { 504 | tx, err := db.Begin(false) 505 | if err != nil { 506 | http.Error(w, err.Error(), http.StatusInternalServerError) 507 | return 508 | } 509 | defer tx.Rollback() 510 | 511 | w.Header().Set("Content-Type", "application/octet-stream") 512 | w.Header().Set("Content-Disposition", `attachment; filename="my.db"`) 513 | w.Header().Set("Content-Length", strconv.Itoa(int(tx.Size()))) 514 | 515 | tx.WriteTo(w) 516 | } 517 | 518 | 519 | * Performance 520 | 521 | * Benchmarks 522 | 523 | These are ballpark estimates! 524 | 525 | 526 | * Write performance 527 | 528 | - 2,000 random writes/sec 529 | - 450,000 sequential, batched writes/sec 530 | 531 | * Read performance 532 | 533 | If data is hot in memory: 534 | 535 | - 1-2µs fetch time 536 | - Iterate over 20M keys/sec 537 | 538 | If data is not hot in memory: 539 | 540 | - Depends on your hard drive's speed 541 | 542 | 543 | * Other fun uses for Bolt 544 | 545 | - Message queue 546 | - Analytics 547 | - Buckets can be nested to provide interesting data structures 548 | 549 | 550 | * Conclusion 551 | 552 | * Conclusion 553 | 554 | - BoltDB fits many application use cases 555 | - Development experience is great 556 | - Testing experience is great 557 | - Deployment experience is great 558 | - Code is not only simple but extremely fast 559 | 560 | * Consider the BGR stack for your next project! 561 | 562 | .image bgr.png -------------------------------------------------------------------------------- /react.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benbjohnson/burger-stack/18000244f8b7e7a37991743e6926a8dedcff479f/react.png --------------------------------------------------------------------------------