Tagged: golang Toggle Comment Threads | Keyboard Shortcuts

  • ThomasPowell 9:39 pm on July 20, 2022 Permalink | Reply
    Tags: , golang, nil   

    Golang nil returns suck ლ(ಠ益ಠლ) 

    Why might you return nil

    In many languages, if you want to represent “nothing” is returned, a common pattern is to use nil as a return value. A common alternative to this is the NullObjectPattern, since a 0/nil/null value generally does not respond to the same number of messages as a legitimate object value. This far more sparse interface is why languages like Ruby have introduced a Safe Navigation Operator. In languages that expose memory directly or indirectly like C++, trying to call object methods on a pointer to NULL could cause mangling of memory offset from that “zero” address.

    What happens in Golang with a nil return

    golang assertions against nil for multiple different representations
    Fighting through trying to match a golang nil return value (GitHub code link)

    If you have a reference or pointer return type, you can specify nil as a return value from it:

    type foo struct {
    }
    
    func fooMeOncePointerWithNil() *foo {
    	return nil
    }
    

    So if you’re writing a test for this (trivial) function, you might be tempted to write

    func TestFooMeOncePointerToArrayWithNil(t *testing.T) {
    	assert.Equal(t, nil, fooMeOncePointerToArrayWithNil(), "Test did not return nil")
    }
    

    But your assertion will fail, because the return value is *not* nil:

    ❯ go test
    --- FAIL: TestFooMeOncePointerToArrayWithNil (0.00s)
        main_test.go:23:
            	Error Trace:	/Users/tpowell/projects/golang/nilTest/main_test.go:23
            	Error:      	Not equal:
            	            	expected: <nil>(<nil>)
            	            	actual  : *[]testy.foo((*[]testy.foo)(nil))
            	Test:       	TestFooMeOncePointerToArrayWithNil
            	Messages:   	Test did not return nil
    FAIL
    exit status 1
    FAIL	testy	0.104s
    

    The actual type is nil cast to a *[]testy.foo cast to a *[]testy.foo.

    You can see this with returning to a pointer to an array of strings more clearly:

    func stringPointerToArrayNil() *[]string {
    	return nil
    }
    
    func TestStringNil(t *testing.T) {
    	assert.Equal(t, nil, stringPointerToArrayNil(), "Function did not return nil")
    }
    

    Which fails similarly, although slightly more readably:

    --- FAIL: TestStringNil (0.00s)
        main_test.go:42:
            	Error Trace:	/Users/tpowell/projects/golang/nilTest/main_test.go:42
            	Error:      	Not equal:
            	            	expected: <nil>(<nil>)
            	            	actual  : *[]string((*[]string)(nil))
            	Test:       	TestStringNil
            	Messages:   	Function did not return nil
    FAIL
    exit status 1
    FAIL	testy	0.246s

    What Technically Works?

    So how we make a nil return value match? Well, you can typecast nil explicitly to match the implicit casting of the returned nil:

    func TestPassFooMeOncePointerToArrayWithNil(t *testing.T) {
    	assert.Equal(t, (*[]foo)(nil), fooMeOncePointerToArrayWithNil(), "Test did not return nil")
    }
    

    Now you’ll get:

    ❯ go test
    PASS
    ok testy 0.141s

    A better “nil”?

    Having to contort a language to do a basic thing is usually an indicator you’re probably using it wrong. In this case, perhaps you just want an empty array:

    func fooMeOnceArray() []foo {
    	return []foo{}
    }
    

    Now you can validate this with the len function on the returned array:

    func TestFooMeOnceArray(t *testing.T) {
    	assert.Equal(t, 0, len(fooMeOnceArray()), "Test did not return nil")
    }
    

    This, of course, passes, but tested against the number of items returned instead of checking for an arbitrary nil value.

    Maybe you just want to check error

    Golang supports multiple return values, and you can add a return of an error type:

    func fooMeOnceArrayError() ([]foo, error) {
    	return []foo{}, nil
    }
    

    The error type *can* be cleanly compared to nil:

    func TestFooMeOnceArrayError(t *testing.T) {
    	_, error := fooMeOnceArrayError()
    	assert.Equal(t, nil, error, "Test did not return nil")
    }
    

    This comparison to nil passes:

    ❯ go test
    PASS
    ok testy 0.238s

    Conclusion

    The bigger lesson in all of this is that if the direction that you’re going down in a language feels unnatural, there might be a better way to do things. Or you might not even be using a pattern that is preferred for the language. Take a step back and look at the bigger picture.

     
  • ThomasPowell 5:06 pm on July 22, 2017 Permalink
    Tags: go concurrency, golang,   

    Go Concurrency Benchmarking on a Raspberry Pi 

    In hindsight, no one claimed that a Raspberry Pi of any model was a powerhouse machine. However, I had thoughts about how to benchmark computing power after leaving Dreamhost and noticing that even tar and gzipping my WordPress installs performed like an ancient machine despite the processor statistics. That made me think about a raw benchmarking metric that could deployed anywhere. I was also learning Go and trying to understand Go concurrency. I ended up with a “concurrency” of 2 because the lower power systems didn’t get any more out of an additional goroutine.


    package main
    import (
    "encoding/base64"
    "fmt"
    "golang.org/x/crypto/sha3"
    "strings"
    "time"
    )
    func main() {
    var before string
    var after [64]byte
    var base64Encoding string
    var result chan string = make(chan string)
    var resultString string
    var addOnStart int64
    start := time.Now()
    addOn := 0
    concurrency := 2
    sem := make(chan bool, concurrency)
    for {
    before = fmt.Sprintf("Message%d", addOn)
    after = sha3.Sum512([]byte(before))
    base64Encoding = base64.StdEncoding.EncodeToString(after[:])
    if strings.HasPrefix(base64Encoding, "TEST") {
    fmt.Printf("%d seconds\n", int64(time.Since(start)/time.Second))
    break
    }
    addOn++
    }
    fmt.Printf("%d: %s\n", addOn, base64Encoding)
    addOnStart = 0
    start = time.Now()
    SEARCHY:
    for {
    sem <- true
    go scan1000000(addOnStart, result, sem)
    select {
    case resultString, _ = <-result:
    break SEARCHY
    default:
    }
    addOnStart++
    }
    fmt.Printf("%d seconds\n", int64(time.Since(start)/time.Second))
    fmt.Print(resultString)
    }
    func scan1000000(addOnStart int64, result chan string, sem chan bool) {
    var before string
    var after [64]byte
    var base64Encoding string
    defer func() { <-sem }()
    for i := addOnStart * 1000000; i < (addOnStart+1)*1000000; i++ {
    before = fmt.Sprintf("Message%d", i)
    after = sha3.Sum512([]byte(before))
    base64Encoding = base64.StdEncoding.EncodeToString(after[:])
    if strings.HasPrefix(base64Encoding, "TEST") {
    result <- fmt.Sprintf("%d: %s\n", i, base64Encoding)
    break
    }
    }
    }

    view raw

    gosha3.go

    hosted with ❤ by GitHub

    The program requires 13,135,013 sha3 hashes and conversions to base 64 to come up with a string that starts with “TEST”.

    My work workstation’s output:

    [Specs: Late 2013 Retina MacBook Pro 15″, 2.3 GHz Core i7 – (I7-4850HQ)]

    • Single thread: 16 seconds
    • Concurrency of two: 10 seconds
    • Compared with PassMark:
      • 9066
      • Single thread 1964

    A Late 2011 MacBook Pro 13″, 2.4 GHz Core i5 (I5-2435M):

    • Single thread: 34 seconds
    • Concurrency of two: 19 seconds
    • Compared with PassMark:
      • 3269
      • Single Thread 1347

    A Core i3-5010U 2.10 GHz NUC (Windows 10 Pro)

    • Single thread: 27 seconds
    • Concurrency of two: 17 seconds
    • Compared with PassMark:
      • 3060
      • Single Thread 1171

    A Mid 2009 MacBook Pro 13″, 2.26 GHz Core 2 Duo (P7550)

    • Single thread: 57 seconds (Originally was 83 on go1.3.3, but better results with 1.8.3)
    • Concurrency of two: 32 seconds (Originally was 80 on go1.3.3)
    • Compared with PassMark:
      • 1521
      • Single Thread 892

    A ThinkPad 11e with A4-6210 APU (1.8 GHz – Windows 10 Home)

    • Single thread: 135 seconds
    • Concurrency of two: 65 seconds
    • Compared with PassMark:
      • 2143
      • Single Thread 697

    Raspberry Pi 3B

    • Single thread: 1265 seconds
    • Concurrency of two: heat warnings!

    For the purposes of this “performance” benchmark, there is definitely a non-linear relationship between a canned benchmark score and the score in a brute force operation that has a heavy calculation component. I’m also isolating only one aspect of system performance. There is a SATA SSD on the newest machine, a SATA HDD on the 2011 and 2009 machines, and an SD card on the Raspberry Pi. The RAM is different in every machine as well. Still, it was a fun experiment that motivated me to spend a little extra time in Go and learning Go Concurrency.

     
c
Compose new post
j
Next post/Next comment
k
Previous post/Previous comment
r
Reply
e
Edit
o
Show/Hide comments
t
Go to top
l
Go to login
h
Show/Hide help
shift + esc
Cancel
%d bloggers like this: