Skip to content

Instantly share code, notes, and snippets.

@dpremus
Created November 12, 2018 14:31
Show Gist options
  • Save dpremus/3b141157e7e47418ca6ccb1fc0210fc7 to your computer and use it in GitHub Desktop.
Save dpremus/3b141157e7e47418ca6ccb1fc0210fc7 to your computer and use it in GitHub Desktop.
Go 2 Error handling
@dpremus
Copy link
Author

dpremus commented Dec 4, 2018

Regarding the Go 2 Error handling draft design maybe we can solve problem with extending go "label" functionality.
Here is code snippet from Mateusz Czapliński as we see there is a lot's of boilerplate error-handling code.

func (t *X) initializeAndActivate(ctx context.Context) error {
	t.logger.Info(fmt.Sprintf("activating X for transaction [%q] @%s", xxx.TransactionCodeFrom(ctx), t.millis()))

	t.bus.Lock()
	defer t.bus.Unlock()
	if t.fooing {
		return fmt.Errorf("trying to activate X for transaction %q while already active", xxx.TransactionCodeFrom(ctx))
	}

	response, err := t.queryOnly(cmd_foo1)
	if err != nil {
		t.hardReset()
		return err
	}
	if response[0] != cmd_foo1[0] {
		t.hardReset()
		return fmt.Errorf("invalid response to FOO1 [% 02X]", response)
	}
	t.fooTable, err = parseFooTable(response[1:])
	if err != nil {
		t.hardReset()
		return err
	}
	t.logger.Info(fmt.Sprintf("X parsed foo table [% 02x] as: %s", response, yyy.JsonString(t.fooTable)))

	err = t.queryAndExpect(cmd_foo2, cmd_foo2)
	if err != nil {
		t.hardReset()
		return err
	}
	err = t.queryAndExpect(cmd_foo3, cmd_foo3)
	if err != nil {
		t.hardReset()
		return err
	}
	err = t.queryAndExpect(cmd_foo4, cmd_foo4)
	if err != nil {
		t.hardReset()
		return err
	}

	t.logger.Info(fmt.Sprintf("activated X for transaction [%q] @%s", xxx.TransactionCodeFrom(ctx), t.millis()))
	t.fooing = true
	return nil
}

Go allow having labels and variables with same name.
Imagine if you assign "not nil" value to variable with same name as existing label
then program automatically jump to that label after assigning.

We need to add some prefix (operator) before variable name to be sure in programmer intention, code readability and avoid backward incompatibility.
For example we can use @ or somehting else.
for example if variable "foo" with a prefix '@' exists but label "foo" doesn't exist compiler should report an error.

Here is a example of prevous code:

func (t *X) initializeAndActivate(ctx context.Context) error {
	t.logger.Info(fmt.Sprintf("activating X for transaction [%q] @%s", xxx.TransactionCodeFrom(ctx), t.millis()))

	t.bus.Lock()
	defer t.bus.Unlock()
	if t.fooing {
		return fmt.Errorf("trying to activate X for transaction %q while already active", xxx.TransactionCodeFrom(ctx))
	}

	response, @err := t.queryOnly(cmd_foo1)
	if response[0] != cmd_foo1[0] {
		@err = fmt.Errorf("invalid response to FOO1 [% 02X]", response)
	}
	t.fooTable, @err = parseFooTable(response[1:])
	t.logger.Info(fmt.Sprintf("X parsed foo table [% 02x] as: %s", response, yyy.JsonString(t.fooTable)))

	@err = t.queryAndExpect(cmd_foo2, cmd_foo2)
	@err = t.queryAndExpect(cmd_foo3, cmd_foo3)
	@err = t.queryAndExpect(cmd_foo4, cmd_foo4)

	t.logger.Info(fmt.Sprintf("activated X for transaction [%q] @%s", xxx.TransactionCodeFrom(ctx), t.millis()))
	t.fooing = true
	return nil

err:
	t.hardReset()
	return err
}

for achieving this behavior compiler internally needs to inject code like this:

if err != nil {
   goto err
}

for example, internally this code:

@err = t.queryAndExpect(cmd_foo2, cmd_foo2)

will be converted to this:

err = t.queryAndExpect(cmd_foo2, cmd_foo2)
if err != nil {
   goto err
}

Maybe we can allow "forward only" jump to a label, to avoid deadlock and mess in the code.

I don't know if this good or bad idea, but I need to write some preprocessor that will convert this syntax to standard code so I can play with this and see how this works in practice.

@networkimprov
Copy link

Moved to section "Labeled error handlers".

Copy link

ghost commented Dec 12, 2018

I like this proposed method above. For my own projects I adopt when possible using labels to jump below the "fall through" return in the function as it seems to make the cleanest code so not far off from this proposal at all. Below is my thinking on various options using labels, I hope this opens up more thoughts on possibilities.

// If you are going handle an error immediately 
// then why create it per function? Might as well just 
// reuse a global error variable most of the time rather 
// than constantly create and destroy the same variable type 
// unless this has problems with channels or some such 
// thing.
var Err error

// Dummy function for example only, 
// do not try to run.
func doSomething (v string) string {
	var result string

	// If we are going to do nothing on error 
	// just do your check and return.
	result, Err = Operation1(v)
	if Err != nil {
		return result
    }
	result, Err = Operation2(v)
	if Err != nil {
		return result
    }

	// If we are going to do "a same thing" on
	// various errors then go to a place where
	// we do a same thing and avoid repeating code.
	result, Err = Operation3(v)
	if Err != nil {
		goto errLog
    }
	result, Err = Operation4(v)
	if Err != nil {
		goto errLog
    }

	// We can do more than one "same thing" and 
	// keep it reasonably clean.
	result, Err = Operation5(v)
	if Err != nil {
		goto errLogExit
    }
	result, Err = Operation6(v)
	if Err != nil {
		goto errLogExit
    }

	// An even cleaner way for when we are doing
	// a "same thing" on error many times would be 
	// to setup an implicit goto but this might make
	// it a little too easy to make a mistake by 
	// forgetting it's going to a particular place.
	// I personally don't have a problem with this but
	// I could see that argument being made.
	// Regardless, the "cleanliness" of this method is 
	// compelling.
	on error goto ErrLogImplicit
	result, Err = Operation7(v)
	result, Err = Operation8(v)

	// Go doesn't promote multiple statements on one line 
	// but sometimes can be the best way for readability.
	on error goto ErrLog2; result, Err = Operation9(v)
	on error goto ErrLog1; result, Err = Operation10(v)
	on error goto ErrLog2; result, Err = Operation11(v)

	// Or like the above example, which I like:
	result, @ErrLog2 = Operation9(v)
	result, @ErrLog1 = Operation10(v)
	result, @ErrLog2 = Operation11(v)
	
	return result

ErrLog:
    Logger(Err)
	return result

ErrLogExit:
    Logger(Err)
	os.Exit(5)

ErrLogImplicit:
    Logger(Err)
	DoAnotherThing()
	return result

ErrLog1:
    Logger(Err)
	DoAThing1()
	return result

ErrLog2:
    Logger(Err)
	DoAThing2()
	return result
}

I should add that I am all for having more than one method of error handling as I don't think "one size fits all" is a good approach to error handling. The standard "check error on return from function" plus one or two methods for jumping to an error handling point to me makes the most sense for having cleaner code.

I've ignored a central error handler for the entire app. like PHP has which I like having done a lot of PHP work but I suspect it wouldn't fit in with the "Go" way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment