quirky go


Josh Gwosdz ([email protected])


red.ht/devconf24-quirky-go


Slides Repo

go is simple and thus easy

except when...

  • handling errors of deferred functions is tricky
  • nil is not always nil
  • short variable declarations funk
  • no real typed enums
  • weird adressability issues
  • interfaces in interface methods are just (nominal?) types

About me

Josh Gwosdz Site Reliability Engineer / Software Engineer
maintainer of package-operator my router is running NixOS (and currently chokes on IPv6)
I have a big big side project / playground graveyard master procrastinator
layouting is not my strength :) ACHTUNG ACHTUNG
3 9 4 8 6

defer errors


			func doIO() error {
				defer unhappiness();
			}
		

			func doIO() error {
				f, err := f.Create("my-file.txt")
				if err != nil { return fmt.Errorf("creating file: %w", err) }
				defer f.Close();
	
				_, err := f.WriteString(`"Meow!" (=^・ω・^=)`)
				return err
			}
		
Testing time!

			func TestReturn(t *testing.T) {
				actual := func() error {
					var err error
					defer func() {
						err = expectedError
					}()
					return err
				}()
			
				assert.Equal(t, expectedError, actual)
			}
		

			func TestReturn(t *testing.T) {
				actual := func() error {
					var err error
					defer func() {
						err = expectedError
					}()
					return err
				}()
			
				assert.Equal(t, expectedError, actual)
			}
		
This doesn't work because the go spec states, that a defered function executes after the return values are set by the return statement.

			func TestNamedReturn(t *testing.T) {
				actual := func() (err error) {
					defer func() {
						err = expectedError
					}()
					return err
				}()
			
				assert.Equal(t, expectedError, actual)
			}
		

Hooray this works!

As long as the outer function itself doesn't return an error, right?


			func TestNamedReturnShadow(t *testing.T) {
				actual := func() (err error) {
					defer func() {
						err = deferedError
					}()

					err = expectedError
					return err
				}()
			
				assert.Equal(t, expectedError, actual)
			}
		

nil is not nil


			func getNil() io.Writer {
				var writer *bufio.Writer // pointer to a struct
				return writer
			}
		

			func getNil() io.Writer {
				var writer *bufio.Writer // pointer to a struct
				fmt.Println("is nil:", writer == nil)
				return writer
			}
		

			func getNil() io.Writer {
				var writer *bufio.Writer // pointer to a struct				
				fmt.Println("is nil:", writer == nil)
				// => is nil: true
				return writer
			}
		

			n := getNil()
		

			n := getNil()
			fmt.Println("is nil:", n == nil)
		

			n := getNil()
			fmt.Println("is nil:", n == nil)
			// is nil: false
		

short variable declarations eagerly declare but fall back to shadowing*

* in some cases

			a := "look ma, no var!"
		

			func tuplerator(a, b int) (int, int) {
				return a, b
			}

			a, b := tuplerator(13, 37)
		

			func tuplerator(a, b int) (int, int) {
				return a, b
			}

			a, b := tuplerator(13, 37)
			a, b := tuplerator(4, 2) // doesn't work
		

			func tuplerator(a, b int) (int, int) {
				return a, b
			}

			a, b := tuplerator(13, 37)
			{
				a, b := tuplerator(4, 2) // does work
				// a: 4, b: 2
			}
			// a: 13, b: 37
		

			func tuplerator(a, b int) (int, int) {
				return a, b
			}

			a, b := tuplerator(13, 37)
			a, c := tuplerator(4, 2)
			// a: 4, b: 37, c: 2
		

			func tuplerator(a, b int) (int, int) {
				return a, b
			}

			a, b := tuplerator(13, 37)
			{
				a, c := tuplerator(4, 2)
				// a: 4, b: 37, c: 2
			}
			// a: 13, b: 37, c: does not exist
		

so what actually happens with .Close() errors?

I don't know.

the man pages for close(2) say:

A careful programmer will check the return value of close(),
since it is quite possible that errors on a previous write(2)
operation are reported only on the final close() that releases
the open file description.  Failing to check the return value
when closing a file may lead to silent loss of data.
[...]

A careful programmer who wants to know about I/O errors may
precede close() with a call to fsync(2).

it's possible to wrap multiple errors


			func TestNamedReturnWrap(t *testing.T) {
				actual := func() (err error) {
					err = expectedError
					defer func() {
						cErr := deferedError
						if err != nil && cErr != nil {
							err = fmt.Errorf(`defered func errored but
								surrounding function also encountered an error.
								surrounding err: %w
								defered err: %w`, err, cErr)
						} else if cErr != nil {
							err = cErr
						}
					}()
					return err
				}()
			
				assert.ErrorIs(t, actual, expected)
				assert.ErrorIs(t, actual, defered)
			}
		

interesting references

aka: this talk does not contain anything original ;)

Red Hat

thanks for coming along

and enjoy the rest of devconf :)