-
Notifications
You must be signed in to change notification settings - Fork 1
/
practical-advice-for-go-library-authors.slide
709 lines (436 loc) · 13.6 KB
/
practical-advice-for-go-library-authors.slide
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
Practical advice for Go library authors
Writing better libraries
14 Jul 2016
Jack Lindamood
Software Engineer, Twitch
* Goal of this talk
Many ways to do common things
Which way is preferred and why
Common pitfalls of Go libraries
What I wish I knew
: Teaching you how to compose music, not play the piano
* Naming things
: There are only two hard things in Computer Science: cache invalidation and naming things.
* General naming advice
Generally imports are not renamed
Standard practice is PACKAGE.FUNCTION()
Think of package as part of the name
// In Java, the package name is silent
Java: new BufferedReader()
// In Go, the package name is where it is used
Go: bufio.NewReader()
: The most important thing is thinking of the package as part of the name
* Naming examples
Tips for structs
- stuttering struct names ok, but avoid if you can
// bad. Too generic
var c client.Client
// stuttering, but accepted
var ctx context.Context
// optimal example
var h http.Client
Tips for functions
- package functions should never stutter
// Ok. Reads as a background context
context.Background()
// Not ok. Context redundant
context.NewContext()
: Primary point is to think of the package name in the public member's name
: Great blog post on naming: https://blog.golang.org/package-names
* Object creation
: Slide 6 of 38
* Object construction
No rigid constructor like other languages
Zero value sometimes works
Constructor function (NewXYZ) has ramifications
Using the struct or constructor function
// Using the struct
x := http.Client{}
// Constructor function
y := bytes.NewBuffer(...)
: Will spend next few slides talking about how to make your object
* Zero value considerations
// Examples of a zero value
var c http.Client
y := bytes.Buffer{}
: Zero value should make sense if people create it directly
Read operations work well on nil map/slice/chan
- Great for making your zero value behave
Less useful if
- struct needs background goroutines
- non zero/empty defaults are the best
Zero struct support is viral!!!!
- So is nil support
* Working with zero values
: Should really be called "Working around no constructor"
Example for defaults
// net/http/server.go
func (srv *Server) ListenAndServe() error {
addr := srv.Addr
if addr == "" {
addr = ":http"
}
// ...
}
Example for nil reads
// bytes/buffer.go
type Buffer struct {
buf []byte
off int
}
func (b *Buffer) Len() int { return len(b.buf) - b.off }
: Talk about sync.Init trade-offs
* New() constructor function
Not as terse as making zero value work
Constructors can do anything. Struct initialization only does one thing.
Risk people use struct w/o getting it from New
Some libraries hide this with a private struct, public interface
: Interfaces are for behavior not information hiding
// anti-pattern.go
type authImpl struct {
}
type Auth interface {
// Tends to be very large
}
func NewAuth() Auth {
}
* What do these do?
func directUse() {
x := grpc.Client{
Port: 123,
Hostname: "elaine82.sjc",
}
// ...
}
func constructor() {
x := grpc.NewClient(123, "elaine82.sjc")
// ...
}
Does constructor() do the following
- spawn goroutines
- allocate extra memory
- panic
: Answer. YOU DON'T KNOW!
: It's like C. Being able to know what code does as quickly as possible
* Singleton options
- Go stdlib makes heavy use of singletons
- not a fan of this
: Only a sith deals in absolutes
- Beware hidden singletons (expvar/http/rand/etc)
: Talk about double import and expvar.Register resulting in panic
- General way singletons are done
: Singletons start off as easy, simple abstractions until they are not then they're really really terrible and difficult
var std = New(os.Stderr, "", LstdFlags)
// Singleton functions in package. Same as struct functions
func Print(v ...interface{}) {
std.Print(v...)
}
func (l *Logger) Print(v ...interface{}) {
// ...
}
* Configuration
: Slide 13 of 38
* Optional config w/ functions
Use NewXYZ(WithThing(), WithThing()) can be clunky
Has not gained adoption
type Server struct {
speed int
}
func Speed(rate int) func(*Server) {
return func(s *Server) {
s.speed = rate
}
}
func NewServer(options ...func(*Server)) {
}
x := NewServer(Speed(10))
: Honestly difficult to use
: http://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis
: https://commandcenter.blogspot.nl/2014/01/self-referential-functions-and-design.html
* Config struct
Gaining popularity
Easy to understand all available options
Does zero work for default config? Sometimes not
Important to document if config is usable after being passed in
Easier to work with config management
// Usually JSON in config management
type Config struct {
Speed int
}
func NewServer(conf *Config) {
}
: Honestly also able to just put your config on your struct
* Logging
: Slide 16 of 38
* Logging counter-example
import "log"
type Server struct {}
func (s *Server) handleRequest(r *Request) {
if r.user == 0 {
log.Println("No user set")
return
}
// ...
}
- Direct print to std streams
- There are many logging frameworks
- Cannot turn it off
* Logging advice
Is it needed?
: Push it up the stack
: error return can remove most logging needs
Logging is a callback
: What does logging communicate?
Log to an interface
Don't assume log package
Respect stdout/stderr
: Worse thing is printing to STDOUT/STDERR
No singleton!
: Structured logging vs built in logging
: Author is a big fan of structured logging
* Better logging example
type Log interface {
Println(v ...interface{})
}
type Server struct {
Logger Log
}
func (s *Server) log(v ...interface{}) {
if s.Logger != nil {
s.Logger.Println(v...)
}
}
func (s *Server) handleRequest(r *Request) {
if r.user == 0 {
s.log("No user set")
return
}
}
Can we do even better?
- Structured logging
* Interfaces
: Slide 20 of 38
* interfaces vs structs
Accept interfaces, return structs
Some libraries only expose interfaces. Keep all structs private.
- Tend to be large interfaces
: Only exposing interfaces tend to get large
Usually no need to include interface from outside your package/stdlib
- Ok to do, just not needed
- Prevents import cycles
- Implicit interfaces get around this
- context.Context made this difficult
Means an API that uses standard objects is usually best
- Easier to implement
: If using standard params, easier for other people to pretend to be your struct
* Let's make a random package
type Rand interface {
ExpFloat64() float64
Float32() float32
Float64() float64
Int() int
Int31() int32
Int31n(n int32) int32
Int63() int64
Int63n(n int64) int64
Intn(n int) int
NormFloat64() float64
Perm(n int) []int
Read(p []byte) (n int, err error)
Seed(seed int64)
Uint32() uint32
}
There are lots of ways to generate random things
What is wrong with this?
* Avoiding large interfaces (rand.Rand)
Lots of ways to get random things
- rand.Rand{} would be a very large interface
rand.Source is an interface. Very small. The building block.
- Lesson: Turn large interfaces into small interfaces with wrapper logic
// A Source represents a source of uniformly-distributed
// pseudo-random int64 values in the range [0, 1<<63).
type Source interface {
Int63() int64
Seed(seed int64)
}
// A Rand is a source of random numbers.
type Rand struct {
src Source
}
* Dealing with problems
: Slide 24 of 38
* When to panic
Never?
panic() in a spawned goroutine is the worst
Panic is usually ok if:
- Function begins with MustXYZ()
- Operations on nil
// MustXYZ calls XYZ, panics on error.
// Note: Users still have the option to call XYZ directly
func MustCompile(str string) *Regexp {
regexp, error := Compile(str)
if error != nil {
panic(`...`)
}
return regexp
}
: Really bad to let panics escape your library
* Checking errors
Check all errors on interface function calls
- Especially the ones you don't expect to error!
If you can't return it, always do something.
- Log it
- Increment something
: We traded exceptions for nil: check them!
: Wrapping errors can add context
When are returning errors appropriate
- When a promise could not be kept
- When an answer could not be given
// Counterexample
HasPermission(userID) error
// Better
HasPermission(userID) (bool, error)
* Enabling debuggability for your library
Especially complex libraries can have state that isn't clear to the user
Suggest exposing an expvar.Var{} as a function
Debug logging can help
Ideally expose Stat() function for atomic integers around tracking information
type Stats struct {
// atomic.AddInt64(&s.stats.TotalRequests, 1)
TotalRequests int64
}
type Server struct {
DebugLog Log
stats Stats
}
func (s *Server) Var() expvar.Var {
return expvar.Func(func() interface{} {
return struct{...}
}
}
* Designing for testing
Complex libraries could benefit users with a simple test helper
- httptest is an example
Abstract away time/IO/os calls with interfaces
- Maintain control
: Ask yourself, is there something you cannot control
: If it's important to keep Scheduler trim, Now can be a package singleton.
// Package stub. For trim structs
var Now = time.Now
// Inside struct for most control
type Scheduler struct {
Now func() time.Time
}
func (s *Scheduler) now() time.Time {
if s.Now != nil {
return s.Now()
}
return time.Now()
}
* Concurrency
: Slide 29 of 38
* Channels
Stdlib has very few channels in the public API
Push what you would use channels for up the stack
Channels are rarely parameters to functions
Callbacks vs channels
Mixing mutexes and channels can be dangerous
: Dangerous to keep mutexes across function calls
Honestly, channels rarely belong in a public API
: Not never, just rarely
: If you're trying to use them, you're trying too hard. If you're trying not to use them, you're trying too hard.
* When to spawn goroutines
Some libraries use New() to spawn their goroutines.
: Always clean up after yourself!
- Not ideal: Stdlib uses .Serve() functions
Close() should end all daemon threads
: My rec: After a Close(), everything is GC-able and goroutines are stopped
: Notice, channels and goroutines: Push it up the stack
Push goroutine creation up the stack
// counterexample
func NewServer(..) *Server {
s := &Server{...}
go s.Start()
return s
}
// ideal
func userLand() {
// ...
s := http.Server{}
go s.Serve(l)
// ...
}
* When to use context.Context and when not to
All blocking/long operations should be cancelable!
Generally an abuse to store context.Context
When to use context.Value()
- What other languages would use thread local for
: But isn't thread local bad? YES IT IS!
- So easy to abuse
- Try hard not to use it
Singletons and context.Value() obscure your program's state machine
- Context.Value() Should *inform* not *control*
: Context should "flow" through your program
Seriously though, try not to use context.Value()
* If something is hard to do, make someone else do it
: Concurrency and synchronization is hard. So don't do it! The best thing you can do in life is make a hard problem someone else's problem?
Great advice in library design, system design, and Dilbert life
The less hard things you try to do, the less likely you'll screw up whatever you're doing.
Hard things (threading/Mutexes/Deadlock/channels/encryption)
Push problems up the stack
- Logging
- Goroutines
- Locking
: Give options, not opinions
Corollary: Try not to do things
* Designing for efficiency
: Slide 34 of 38
Correctness still trumps efficiency
Minimizing memory allocs is usually first priority
: - Encode into a buffer vs returning []byte
: - Designing around WriteTo vs Write()
: - Using len parameter on make()
Avoid creating an API that forces memory allocations
- Easy to optimize internals
- Hard to change an API
: If you write fast and buggy code then fix it, you're a liability. If you write correct and slow code, then speed it up you're a 10x engineer that keeps on delivering.
func (s *Object) Encode() []byte {
// ....
}
func (s *Object) WriteTo(w io.Writer) {
// ...
}
* Using /vendor in libraries
Package management is not ideal for go libraries
: 100% blame "go get"
: Package management: The o rings of the go rocket ship?
Don't use /vendor for libraries.
- Will have issues if the library has singletons
: That's why I hate singletons BTW
- Try to use implicit interfaces and injection
Don't expose vendored deps as part of the library's external API
- Their github.com/a isn't the same as your github.com/b/vendor/github.com/a
: Ideally keep vendoring internal
Honestly, just don't use libraries in your library
- npm::left-pad
: http://www.theregister.co.uk/2016/03/23/npm_left_pad_chaos/
: I break this rule for testing
: Testing helpers are awesome and useful and totally worth having in another library
: Just like repeatably reliable builds are important for binaries, repeatably correct assertions are important for libraries. This means you have control over your dependencies.
* Build tags
Cross OS libraries
Writing libraries that are compatible with new versions of Go
- Example: http.Request.Cancel channel
- Use build tags like // +build go1.5
Integration tests
- Integration testing for libraries (with // +build integration)
* Staying clean
Numerous static analysis tools for go
- Use almost all of them
- Once experienced, you can lint/grep them away
Build options (travis/circle/etc)
- Many free continuous testing integrations for open source libraries
- Helps ensure code works for various Go versions
: gometalinter is pretty awesome
: Follow my blog on Medium for more detailed posts https://medium.com/@cep21