Beyang Liu for the GopherCon Liveblog on August 29, 2018
Nyah Check (@nyah_check, slides) is a software engineer at Altitude Networks.
Nyah comes from a C/C++ background and subsequently wrote a lot of bad Go code early on. He hopes others can learn from his mistakes.
He has classified his mistakes under 3 topics:
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.
An early mistake was to minimize escape analysis and it's possible implications on my program's perf.
Consider the following C++ code:
int foo() {
// This is a memory leak below
int *a = new(int);
return *a;
}
Wrong assumptions:
Let's look at some code...
package main
import "fmt"
func newIntStack() *int {
vv := new(int)
return vv
}
func main() { fmt.Println(*newIntStack()) }
This is a program that tries to establish if allocation takes place on the heap or the stack. When he runs this program (go run -gcflags -m file.go
), you see that the new(int)
variable does not escape (i.e., it's on the stack, not the heap). In C++, it would be allocated on the heap.
Let's take a look at another example:
package main
import "fmt"
func main() {
x := "GOPHERCON-2018"
fmt.Println(x)
}
In the above example, x
escapes to the heap. That's because fmt.Println
takes an interface, which means x
gets transferred to the heap.
Escape analysis is not trivial in Go. You need to do runtime analysis; can't just look at the code.
Lessons
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"
How does memory leak in Go
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:
Something like this might work:
#define TIME_TO_WAIT 1 /* wait for one second */
int main() {
FILE *fp;
clock_t last = clock();
char* directory[2] = {"one.txt", "two.txt"};
for ( ; ; ) {
clock_t current = clock();
if (current >= (last + TIME_TO_WAIT + CLOCKS_PER_SEC)) {
for (int i = 0; i < 2; i++) {
fp = fopen(directory[i], "r+");
printf("\nopening %s", directory[i]);
if (fp == NULL) {
fprintf(stderr, "Invalid file %s", directory[i]);
exit(EXIT_FAILURE);
}
//some FILE processing happens
fclose(fp);
printf("\nclosing %s", directory[i]);
last = current;
}
}//executes every second
}
This will be sure to open and close up the files (open, close, open, close, etc.) once the operations are done.
However in Go:
func loggingMonitorErr(files ...string) {
for range time.Tick(time.Second) {
for _, f := range files {
//files coming in through the channel.
fp := OpenFile(f)
// The line below will never execute.
defer fp.Close()
//process file
}
}
}
The output from running the program shows there is no closing of files.
Problems:
How do I fix this?
The fixed solution looks like this:
type file string
func OpenFile(s string) file {
log.Printf("opening %s", s)
return file(s)
}
func (f file) Close() { log.Printf("closing %s", f) }
func loggingMonitorFix(files ...string) {
for range time.Tick(time.Second) {
for _, f := range files {
//files coming in through the channel.
func() {
fp := OpenFile(f)
defer fp.Close()
//process file
}()
}
}
}
Lessons learned:
Conclusion: "Do not defer in an infinite loop, since the defer statement invokes the function execution ONLY when the surrounding function returns"
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:
Understanding this can avoid some robustness issues.
Prior to Go 1.2 there was a memory safety issue with slices:
Example:
func main() {
a := []*int{new(int), new(int)}
fmt.Println(a)
b := a[:1]
fmt.Println(b)
// second element is not garbage collected, because it's *still* accessible
c := b[:2]
fmt.Println(c)
}
If you run this code, the third Println
shows in c
you somehow have access to elements in a
that aren't accessible in b
.
What are some of the problems?
How do you solve this then? Go 1.2++ added the 3-Index-Slice operation:
Rewriting our code gives:
func main() {
a := []*int{new(int), new(int)}
fmt.Println(a)
// Using the 3- index slice operation
b := a[:1:1]
fmt.Println(b)
c := b[:2]
fmt.Println(c)
}
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
What's a Goroutine?
There's no language-level analog in C/C++. You have to use special libraries to write multi-threaded code.
How do goroutines leak? There are different possible causes for goroutine leaks, some include:
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.
Consider:
func doSomethingTwice() error {
// Issue occurs below
errc := make(chan error)
go func() {
defer fmt.Println("done wth a")
errc <- doSomething("a")
}()
go func() {
defer fmt.Println("done with b")
errc <- doSomething("b")
}()
err := <-errc
return err
}
What are the problems with the code?
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.
func doSomethingTwice() error {
// Issue occurs below
errc := make(chan error, 2)
go func() {
defer fmt.Println("done wth a")
errc <- doSomething("a")
}()
go func() {
defer fmt.Println("done with b")
errc <- doSomething("b")
}()
err := <-errc
return err
}
Goroutine leaks are very common in Go development.
However there are some best practices you can follow to avoid some of these errors:
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.
Consider a C program with a division by zero error:
#include <stdio.h> /* for fprintf and stderr */
#include <stdlib.h> /* for exit */
int main( void )
{
int dividend = 50;
int divisor = 0;
int quotient;
if (divisor == 0) {
/* Example error handling.
* Writing a message to stderr, and
* exiting with failure.
*/
fprintf(stderr, "Division by zero! Aborting...\n");
exit(EXIT_FAILURE); /* indicate failure.*/
}
quotient = dividend / divisor;
exit(EXIT_SUCCESS); /* indicate success.*/
}
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:
func main() {
conn, err := net.Dial("tcp", "goooooooooooogle.com:80")
if err != nil {
fmt.Printf("%T\n", err)
log.Fatal(err)
}
defer conn.Close()
}
Using %T
in the format string, you can print the type of the error, which often provides useful information.
Wrapping Errors in Go with github.com/pkg/errors
:
Consider another example
func connect(addr string) error {
conn, err := net.Dial("tcp", addr)
if err != nil {
switch err := err.(type) {
case *net.OpError:
// return fmt.Errorf("failed to connect to %s: %v", err.Net, err)
return errors.Wrapf(err, "failed to connect to %s", err.Net)
default:
// return fmt.Errorf("unknown error: %v", err)
return errors.Wrap(err, "unknown error")
}
}
defer conn.Close()
return nil
}
Advantages of Wrap and Cause funcs:
Nyah believes it’s a feature some developers my overlook but if used properly will give a better Go development experience.
Lessons learned:
Take a look at the errors package and see elegant examples.
There are many more errors C/C++ devs make. Just remember:
What tools do you miss from C/C++?