This repository has been archived by the owner on Oct 6, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy path5-mistakes-c-cpp-devs-make-writing-go.slide
476 lines (278 loc) · 13.5 KB
/
5-mistakes-c-cpp-devs-make-writing-go.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
5 Mistakes C/C++ Devs make writing Go
A newbie's journey into Go
Aug 29 2018
Nyah Check
Software Engineer, Altitude Networks
nyah@altitudenetworks.com
https://github.com/Ch3ck
@nyah_check
* Why am I here?
- Wrote C/C++ for close to 5 years before Go.
- Brought bad C style code in Go and had a lot of issues
* What you'll learn...
- Learn from my mistakes
- Avoid some common pitfalls newbies face writing Go
Follow the slides here: *https://bit.ly/5-go-mistakes*
* Agenda
I classified my mistakes under 3 topics:
- Heap Vs Stack
- Memory & Goroutine leaks
- Error handling
* One more thing ...
This is a discussion
If you don't understand something, or think what I'm saying is incorrect, please ask at the end.
I'll leave some minutes at the end of the presentation for some Q/A
* Heap Vs Stack
* What is a Heap and Stack in Go?
A *Stack* is a special region in created to store *temporary* variables *bound* to a function.
It's self cleaning and expands and shrinks accordingly.
A *Heap* is a bigger region in memory in addition to the stack used for storing values,
It's more costly to maintain since the GC needs to clean the region from time to time adding extra latency.
.image stack-heap.jpg 300 400
* Mistake 1: New doesn't mean heap && var doesn't mean stack
An early mistake was to minimize *escape*analysis* and it's possible implications on my program's perf.
Consider the following _C++_ code
.code 01-new-doesnt-mean-heap/examples/heap.cpp /START OMIT/,/END OMIT/
* Wrong assumptions..
- In C++, we know *new(int)* is allocated on the heap.
- In Go, we don't really know for sure.
- May be the *new* keyword was stolen from C++ as a result might likely be allocated on the heap?
- Given my C++ bias, I thought minimizing it's use will reduce _heap_ allocation.
* Let's look at some code...
.play 01-new-doesnt-mean-heap/examples/stack.go
# * Question
#
# Where do we think the *vv* variable will be allocated?
#
# *Stack* or *Heap*?
#
# * Let's look at the compiler escape decisions output
#
# ➜ examples git:(master) ✗ go run -gcflags -m stack.go
# # command-line-arguments
# ./stack.go:7:6: can inline newIntStack
# ./stack.go:14:39: inlining call to newIntStack
# ./stack.go:9:11: new(int) escapes to hea
# ./stack.go:14:27: *(*int)(~r0) escapes to heap
# ./stack.go:14:39: main new(int) does not escape <-- STACK ALLOCATED
# ./stack.go:14:26: main ... argument does not escape
# 0
# ➜ examples git:(master)
#
* Let's take a look at another example
.play 01-new-doesnt-mean-heap/examples/heap.go
# * Where will x be allocated?
#
# *Heap* or *Stack*?
#
# * Let's find out...
#
# ➜ examples git:(master) ✗ go run -gcflags -m heap.go
# # command-line-arguments
# ./heap.go:9:13: x escapes to heap <---- Something strange happens
# ./heap.go:9:13: main ... argument does not escape
# GOPHERCON-2018
#
# It's surprising to see *x* which not called outside may is allocated on the heap instead.
#
# * Why?
#
# I’ll pass the -m option multiple times to make the output more verbose:
#
# # command-line-arguments
# ./heap.go:7:6: cannot inline main: non-leaf function
# ./heap.go:9:13: x escapes to heap
# ./heap.go:9:13: from ... argument (arg to ...) at ./heap.go:9:13
# ./heap.go:9:13: from *(... argument) (indirection) at ./heap.go:9:13
# ./heap.go:9:13: from ... argument (passed to call[argument content escapes]) at ./heap.go:9:13
# ./heap.go:9:13: main ... argument does not escape
# GOPHERCON-2018
# ➜ examples git:(master) ✗
#
#
# * What happened?
#
# So looking at *Line*13*
#
# - x is passed to `fmt.Println` which receives an *interface* argument
# - so x is converted to an interface whose value 'x' is alloc on the heap.
# - So x `escapes`
#
# This is very confusing/counterintuitive to a C/C++ developer, yet this is how Go works.
#
* Lessons
- Escape analysis is very important in writing more performant Go programs, yet there's no language specification on this.
- Some of the compiler's escape analysis decisions are counterintuitive, yet trial and error is the only way to know
- Do not make assumptions, rather do *escape*analysis* on the code and make informed decisions.
* Conclusion
"Understand heap vs stack allocation in your Go program by checking the compiler's escape analysis report and making informed decisions, do not guess"
.caption
* Memory Leaks
* How does memory leak in Go
- I assumed since there's a garbage collector, then everything is fine
*Not*True!*
- Memory leaks are common in any language including garbage collected languages
- It can be caused by: assigned but unused memory, synchronization issues.
- Some of these errors can be hard to detect, but Go has a set of tools which could be very effective in debugging these bugs
* Mistake 2: Do not defer in an infinite Loop
The *defer* statement is used to clean up resources after you open up a resource(e.g. file, connection etc)
So an idiomatic way will be:
fp, err := os.Open("path/to/file.text")
if err != nil {
//handle error gracefully
}
defer fp.Close()
This snippet is guaranteed to work even if cases where there’s a panic and it’s *standard* Go practice.
* So what's the problem?
In very large files where resources cannot be tracked and freed properly, this becomes a problem.
Consider a file monitoring program in *C* where:
- We check a specific directory for db file dumps
- perform some operation(logging, file versioning, etc)
* Something like this might work
.code 02-do-not-defer-in-infinite-loop/examples/file.c /START OMIT/,/END OMIT/
This will be sure to open and close up the files once the operations are done.
* However in Go
.play 02-do-not-defer-in-infinite-loop/examples/error.go /START OMIT/,/END OMIT/
*Problems:*
- Deferred code never executes since the function has not returned
- So memory clean up never happens and it’s use keeps piling up
- Files will never be closed, therefore causing loss of data due to lack of flush.
* How do I fix this?
- Creating a function literal for each file monitoring process
- This ensures everything is bound to the context
- Hence files are opened and closed
* Solution
.play 02-do-not-defer-in-infinite-loop/examples/main.go /START OMIT/,/END OMIT/
* Lessons learned
- Since defer is tied to the new function context, we are sure it's executed and memory is flushed when files close
- When defer executes we are certain our function literal finished execution, so no memory leaks
* Conclusion
"Do not defer in an infinite loop, since the defer statement invokes the function execution *ONLY* when the surrounding function returns"
.caption
* Pointers to accessible parts of a slice
* What's a slice?
A *slice* is a dynamically sized flexible view into an array.
We know arrays have fixed fizes.
There are *two* main *features* of slices to think about:
- The *length* of a slice is simply the total number of elements contained in the slice
- The *capacity* of a slice is the number of elements in the underlying array.
Their understanding can avoid some robustness issues.
* How?
* Mistake 3: Keeping pointers to an accessible(although not visible) part of a slice
Prior to Go 1.2 there was a memory safety issue with slices
- access to elements of the underlying array.
- This could lead to unintended memory writes.
- Cause *robustness* issues
- These regions of memory are not garbage collected.
* Let's use an example.
.play 03-pointer-in-non-visible-slice-portion/examples/main.go /START OMIT/,/END OMIT/
* What are some of the problems?
- Write regions of memory unintentionally.
- Robustness issues: Memory is not garbage collected since there's a *reference* to it.
- It's a source for potential bugs
* How do you solve this then?
Go 1.2++ added the *3-Index-Slice* operation
- This enables you to specify the capacity during slicing.
- The restricted slice capacity provides a level of protection to the underlying array
- No unintended memory writes.
- Unused areas of the underlying array are garbage collected.
* How do we use it then
Rewriting our code gives
.play 03-pointer-in-non-visible-slice-portion/examples/fixedSlice.go /START OMIT/,/END OMIT/
* Our output becomes ...
➜ examples git:(master) ✗ go run main.go
[0xc420016090 0xc420016098]
[0xc420016090]
panic: runtime error: slice bounds out of range
goroutine 1 [running]:
main.main()
/Users/nyahcheck/go/src/github.com/Ch3ck/5-mistakes-c-cpp-devs-make-writing-go/03-pointer-in-non-visible-slice-portion/examples/main.go:27 +0x1ae
exit status 2
Our slice cap was set to 1, we can't access regions of memory we don't have permissions to, rightly creating a panic.
* Lesson
- Our slice capacity was set to 1, so can't access restricted regions in memory, rightly creating a panic
- More robust programs
- Fewer memory leaks since unused memory is garbage collected.
- Reduce sources for potential bugs in your code.
* Goroutine leaks
* What's a Goroutine
It's a *lightweight* thread of execution, it consists of functions that run *concurrently* with other functions/methods.
What about channels?
A *channel* is a pipe that connects concurrent goroutines.
An understanding of these two concepts embodies concurrency in Go.
* How do they leak?
There are different possible causes for goroutine leaks, some include:
- Infinite loops
- Blocks on synchronization points(channels, mutexes), deadlocks
However when these occur the program takes up more memory than it actually needs leading to *high*latency* and frequent crashes.
Let's take a look at an example
* Mistake 4: Error handling with channels where # channels < # goroutines
- C/C++ has libraries for multi-threaded programming.
- Concurrency in Go materializes itself in the form of goroutines and channels.
*Problem:*
- We'll look at the issue
- Fix it and discuss go tools available to handle these kinds of issues
* main.go
.play 04-error-handling-with-channels/examples/main.go /START OMIT/,/END OMIT/
* What are the problems with the code
- More goroutines than channels are present to write to send data back to main
- When one routine writes to the channel, the program exits and the other goroutine is lost, building up memory use as a results
- that region of memory is not garbage collected
* How do we fix this?
We simply increase the number of channels to 2,
This makes it possible for the two goroutines to pass their results to the calling program.
.play 04-error-handling-with-channels/examples/fixed.go /START OMIT/,/END OMIT/
* Performing Traces on the code
* Lessons
Goroutine leaks are very common in Go development.
However there are some best practices you can follow to avoid some of these errors:
- Using the context package to terminate or timeout goroutines which may otherwise run indefinitely
- Using a done signal or timeout channel can help in terminating a running goroutine preventing leaks
* Best practices (cont.)
- Profiling the code, Stack trace instrumentation and adding benchmarks can go a long way in finding these leaks
- Take advantage of the go tooling ecosystem: go tool trace, go tool profile , go-torch, gops, leaktest etc
- Worth checking the *errgroup* package for this pattern
* Error handling
* What are errors in Go?
Go has a built-in *error* type which uses error values to indicate an abnormal state.
Also these error type is based on an error *interface.
type error interface {
Error() string
}
The `Error` method in `error` returns a *string*
A closer look at the *errors*package* will provide some good insides into handling errors in Go.
* Mistake 5: Errors are not just strings, but much more
Consider a *C* program with a division by zero error
.play 05-errors-are-not-strings/examples/error_in_c.c
* Handling errors in C
- Typically consists of writing error message to stderr and returning an exit code.
However, in Go errors are much more sophisticated than strings.
Consider this example:
.play 05-errors-are-not-strings/examples/main.go /START OMIT/,/END OMIT/
* Wrapping Errors in Go with github.com/pkg/errors
Consider another example
.play 05-errors-are-not-strings/examples/errors.go /START OMIT/,/END OMIT/
* Advantages of Wrap and Cause funcs
- You can preserve the error context and pass to the calling program
- Using the `errors.Cause()` function call we can determine what caused this error later in the program
I believe it’s a feature some developers my overlook but if used properly will give a better Go development experience.
* Lesson learned:
- The errors package provides a lot of powerful tools for handling errors which some devs may ignore.
- Wrap() and errors.Cause() are very useful in preserving context of an error later in the program.
Take a look at the errors package and see elegant examples.
* Conclusion
- *Understand* Escape analysis by looking at the compiler decisions, do not make *reasonable*guesses*.
- *Defer* executes only when the function returns. Using it in a infinite loop is a *mistake*.
- Three Index_slices adds an extra *robustiness* utility in Go, use it.
* Conclusion (cont.)
- *Profile* your Go code to identify bottlenecks early on, it's a good practice.
- Errors in Go are not just strings, but much more.
- Wrap errors to preserve context and handle them gracefully.
* There are many more errors C/C++ devs make
Just remember ...
- Bringing concepts from C/C++ is fine but be ready to be challenged by differences.
- "Programming in Go is like being young again (but more productive!)."
.caption
* Discussion
Any questions?
*Slides:* *https://bit.ly/5-go-mistakes*