NUTS! – #golang lessons learned and design patterns @bitly – Fixed... or is it?



NUTS! – #golang lessons learned and design patterns @bitly – Fixed... or is it?

0 1


nuts

A presentation on golang lessons learned and design patterns at bitly

On Github danielhfrank / nuts

NUTS!

#golang lessons learned and design patterns @bitly

Dan Frank * @danielhfrank * github.com/danielhfrank

Not for the uninitiated! I hope you have:

  • Read a tutorial / written something in Go
  • Familiarity with channels (buffered and unbuffered)
  • Familiarity with Go's duck typing system
Not that I'm actually an expert! Please chime in with questions or suggestions, I'm hoping to make this a learning experience for me as well

Motivation

We've hit many pitfalls. They've led to many better design decisions, but we haven't recorded them anywhere. Sharing x Discussion = Win for everyone

Part 1

Unbreak My Heart

Exit logic and concurrency

func doStuff(workChan chan *work, exitChan chan struct{}){
    for {
        select{
        case msg := <- workChan:
            // do some work
        case <- exitChan:
            break
        }
    }
    // cleanup
}
				    

Seems pretty clear what the author intended, but what will actually happen here?

The break will only break out of the select statement!

No shortage of options to fix, I prefer replacing with return

Fixed... or is it?

func doStuff(workChan chan *work, exitChan chan struct{}){
    defer func(){ //cleanup
        }()
    for {
        select{
        case msg := <- workChan:
            // do some work
        case <- exitChan:
            return
        }
    }
}
				    

What happens when both channels have messages?

Select picks from one of them arbitrarily!

Too Crazy?

func doStuff(workChan chan *work, exitChan chan struct{}){
    defer func(){ //cleanup
        }()
    for {
        select{
        case msg := <- workChan:
            // do some work
        default:
            select{
            case <- exitChan:
                return
            default:
            }
        }
    }
}
				    

A Cleaner Way Out

We can communicate the end of messages on a channel using the close statement, and iterate using the range statement

func doStuff(workChan chan *work){
    for msg := range workChan {
        // do some work
    }
    // cleanup
}
				    

Close the incoming channel elsewhere when you are ready to quit

One size doesn't fit all!

func doStuff(workChan chan *work, otherChan *work, exitChan chan struct{}){
    defer func(){ //cleanup
        }()
    for {
        select{
        case msg := <- workChan:
            // do some work
        case otherMsg := <- otherChan:
            // do some other kind of work
        case <- exitChan:
            return
        }
    }
}
    				    

How to correctly handle arbitrary selects when muxing over multiple channels?

Case Study: file2http

Reads lines from stdin and publishes them to an http endpoint bit.ly/file2http

First attempt: ALL THE GOROUTINES

		                

func main(){
    reader := bufio.NewReader(os.Stdin)
    for {
        line, err := reader.ReadString('\n')
        if err != nil {
            break
        }
       go publish(line)
    }
}
				    

Freedom just ain't free

  • Goroutine creation and context switching is cheap but not free.
  • Creating too many goroutines will bog down your application (not to mention the difficulty in synchronizing them all)
  • We try to avoid situations where we create an unbounded number of goroutines

Better: parellelize requests, but know numPublishers

func publish(msgChan chan []byte) {
    for msg := range msgChan {
        // publish it (might take a long time!)
    }
}		                

func main(){
    msgChan := make(chan []byte, 5)
    for i := 0, i < numPublishers, i++ {
        go publish(msgChan)
    }
    for {
        // read line and handle error
        msgChan <- line
    }
    close(msgChan)
}
				    

Program will exit with msgs still in the channel buffer!

Unfortunately, unbuffering doesn't quite solve our problems

The goroutine that picks the value off the channel can and will get interrupted by the main goroutine exiting

Quite simply, worker goroutines need to be able to tell the main goroutine when they are done

OK, take two

func publish(msgChan chan []byte, doneChan chan bool) {
    for msg := range msgChan {
        // publish it (might take a long time!)
    }
    doneChan <- true // signal completion
}		                

func main(){
    msgChan := make(chan []byte, 5)
    doneChan := make(chan bool)
    for i := 0, i < numPublishers, i++ {
        go publish(msgChan, doneChan)
    }
    for {
        // read line and handle error
        msgChan <- line
    }
    close(msgChan)
    // wait for the right number of completion signals
    for i := 0, i < numPublishers, i++ {
        <-doneChan
    }
}
    				    

My preferred way...

func publish(msgChan chan []byte, doneChan chan bool) {
    for msg := range msgChan {
        // publish it (might take a long time!)
    }
    doneChan <- true
}		                

func main(){
    msgChan := make(chan []byte, 5)
    doneChan := make(chan bool) 
    for i := 0, i < numPublishers, i++ {
        go publish(msgChan, doneChan)
        // each time we fire off publish, make sure we wait
        defer func(){<-doneChan}() 
    }
    for {
        // read line and handle error
        msgChan <- line
    }
    close(msgChan)
}
    				    

Streamline with sync.WaitGroup

func publish(msgChan chan []byte, waitGroup *sync.WaitGroup) {
    for msg := range msgChan {
        // publish it (might take a long time!)
    }
    waitGroup.Done()
}		                

func main(){
    msgChan := make(chan []byte, 5)
    waitGroup := &sync.WaitGroup{}
    waitGroup.Add(numPublishers)
    for i := 0, i < numPublishers, i++ {
        go publish(msgChan, waitGroup)
    }
    for {
        // read line and handle error
        msgChan <- line
    }
    close(msgChan)
    waitGroup.Wait()
}
				    

</Part 1>

Now is a good time to politely run for the exits

Or ask questions

Or take issue with anything I've said up to this point

Part 2

Unimorphism

"Object Oriented Programming" in Go

...otherwise known as, "use interfaces"

or, James Gosling's nightmare

Polymorphism as I understand it

The caller of a method doesn't need to know exactly what type of object it is calling, the "right" method will still get called

Within constraints of the language's type system, of course

Going boldly where many have failed before...

Case Study: NY Basketball fans

Base type: Knicks Fan

Subtype: Newly Converted Nets Fan

Let's look at how they behave in a traditional object oriented language

public class KnicksFan extends Object {
    public void cheer(){
        System.out.println("Go Knicks!");
    }
}

public class NewlyConvertedNetsFan extends KnicksFan {
    @Override
    public void cheer(){
        System.out.println("Go Nets!");
    }    
}

public class MadisonSquareGarden extends Object {
    public static void makeSomeNoise(KnicksFan fan){
        fan.cheer();
    }

    public static void main(String... args) {
        NewlyConvertedNetsFan conflictedFan = new NewlyConvertedNetsFan();
        makeSomeNoise(conflictedFan); // prints "Go Nets!"
    }
}
                    

Works in Python too

class KnicksFan(object):
    def cheer(self):
        print "Go Knicks!"

class NewlyConvertedNetsFan(KnicksFan):
    def cheer(self):
        print "Go Nets!"

if __name__ == '__main__':
    fan = NewlyConvertedNetsFan()
    print isinstance(fan, KnicksFan) # True
    fan.cheer() # Go Nets!
                    

Now let's try the same thing in Go!

We've heard that embedding is just like inheritance but better, so we'll use that

type KnicksFan struct{}

func (k *KnicksFan) Cheer(){
    log.Println("Go Knicks")
}

type NewlyConvertedNetsFan struct{
    KnicksFan
}

func (n *NewlyConvertedNetsFan) Cheer(){
    log.Println("Go Nets")
}

func MakeSomeNoise(f *KnicksFan){
    f.Cheer()
}

func main(){
    conflictedFan := &NewlyConvertedNetsFan{}
    MakeSomeNoise(conflictedFan)
}
                    

Doesn't compile!

Choice advice from a golang author

Ken taught me that thinking before debugging is extremely important. If you dive into the bug, you tend to fix the local issue in the code, but if you think about the bug first, how the bug came to be, you often find and correct a higher-level problem in the code that will improve the design and prevent further bugs.

-Rob Pike

Screw that, we just want it to compile!

type KnicksFan struct{}

func (k *KnicksFan) Cheer(){
    log.Println("Go Knicks")
}

type NewlyConvertedNetsFan struct{
    KnicksFan
}

func (n *NewlyConvertedNetsFan) Cheer(){
    log.Println("Go Nets")
}

func MakeSomeNoise(f *KnicksFan){
    f.Cheer()
}

func main(){
    conflictedFan := &NewlyConvertedNetsFan{}
    MakeSomeNoise(&conflictedFan.KnicksFan) // <==
}
                    

Compiles, but it prints "Go Knicks"

WTF

The problem is, we're using the wrong metaphor

  • Traditional OO: The NewlyConvertertedNetsFan is a special kind of Knicks fan
  • Golang: The NewlyConvertedNetsFan still has a little Knicks fan inside of it
  • When we pass in &conflictedFan.KnicksFan, we are telling that Knicks fan inside to cheer, and it is saying "Go Knicks!"

How can we acheive polymorphism-like behavior?

Use interfaces

Specifically, the interface defines exactly which methods can have polymorphic behavior

Fixed!

type Fan interface{
    Cheer()
}

type KnicksFan struct{}

func (k *KnicksFan) Cheer(){
    log.Println("Go Knicks")
}

type NewlyConvertedNetsFan struct{
    KnicksFan
}

func (n *NewlyConvertedNetsFan) Cheer(){
    log.Println("Go Nets")
}

func MakeSomeNoise(f Fan){ // <==
    f.Cheer()
}

func main(){
    conflictedFan := &NewlyConvertedNetsFan{}
    MakeSomeNoise(conflictedFan)
}
                    

THANK YOU

These slides: bit.ly/go_nuts

Made with reveal.js

Dan Frank * @danielhfrank * github.com/danielhfrank

Bitly Go Open Source

  • go-notify
  • go-simplejson
  • file2http
  • mreiferson/go-httpclient
  • mreiferson/go-install-as
  • mynameisfiber/Go-Redis