Golang does not contain and is not intended to contain a Try-Catch-Finally mechanism but does have a stack unwinding panic and recover mechanism which people have used to simulate exception handling.
As a pure Golang learning exercise I reinvented the try-catch-finally wheel (and then updated it with parts of the more elegant code from the example) which does work correctly as far as I can tell. That this is not a canonical golang pattern is not relevant to this activity.
This code review request is about how to test such a construct thoroughly (but of course any logic flaws in the try-catch-finally is welcome) and in a well structured manner. This test passes and provides full coverage but I am uncertain if I have tested all non-trivial cases or special behaviours relating to language possibilities/behaviour and I would like any stylistic / test structure improvement suggestions that you may wish to provide.
The code to be unit tested:
package goUtil
// PanicException performs a traditional try, catch, finally block.
// You can't rethrow a panic so you have to consider when you really want to catch.
// If you catch a panic then unless you harvest the stack trace then you have lost it.
func PanicException(try func(), catch func(interface{}), finally func()) {
if finally != nil {
// Ensure any finally is executed
defer finally()
}
if catch != nil {
defer func() {
if r := recover(); r != nil {
// Execute the catch passing in the panic object
catch(r)
}
}()
}
// try and invoke the function call
try()
}
My unit test code for review:
package goUtil
import (
"fmt"
"testing"
)
func TestTryCatchFinallyOperation(t *testing.T) {
i := 1
obj := &i
innerTryCalled := false
innerTryCompletes := false
innerCatchCalled := false
innerCatchCompleted := false
middleTryCompletes := false
middleCatchCalled := false
middleFinallyCalled := false
outerTryCompletes := false
outerFinallyCalled := false
// The inner try panics, the inner catch re-panics but the middle catch swallows the panic
PanicException(func() {
// Outer try
PanicException(func() {
// Middle try
PanicException(func() {
// Inner try; throw panic
innerTryCalled = true
panic(obj)
innerTryCompletes = true
}, func(e3 interface{}) {
// Inner catch; re-panic
innerCatchCalled = true
FailIfFalse(t, e3 == obj, "The panic object has wrongly changed")
panic(e3)
innerCatchCompleted = true
}, nil)
middleTryCompletes = true
}, func(e2 interface{}) {
// Middle catch; swallow panic
middleCatchCalled = true
FailIfFalse(t, e2 == obj, "The panic object has wrongly changed")
}, func() {
// Middle finally
middleFinallyCalled = true
})
outerTryCompletes = true
}, nil,
func() {
// Outer fina
outerFinallyCalled = true
})
// Check the execution path
FailIfFalse(t, innerTryCalled == true, "The inner try was wrongly never called")
FailIfFalse(t, innerTryCompletes == false, "The inner try should not have completed execution => panic did not unwind stack")
FailIfFalse(t, innerCatchCalled == true, "The inner catch failed to catch the panic")
FailIfFalse(t, innerCatchCompleted == false, "The inner catch failed to re-panic")
FailIfFalse(t, middleTryCompletes == false, "The middle try should not have completed => the inner catch failed to re-panic")
FailIfFalse(t, middleCatchCalled == true, "The middle catch failed to cath the panic")
FailIfFalse(t, middleFinallyCalled == true, "The middle finally was not called when there was no panic")
FailIfFalse(t, outerTryCompletes == true, "The outer try did not complete execution => the middle catch falied to catch the panic")
FailIfFalse(t, outerFinallyCalled == true, "The outer finally failed to run when there was no panic")
// No panic should escape the TryCatchFinally stack above which would trigger the test to fail automatically
}
type TestObj interface {
Log(args ...interface{})
Fail()
}
// FailIfFalse will invoke t.Fail() if pass is false and generate a log message if failFormat is not ""/nil.
// The failArgs are optional and if present will be args into a t.Logf(failFormat, failArgs...) equivalent call.
// The value of pass is returned.
func FailIfFalse(t TestObj, pass bool, failFormat string, failArgs ...interface{}) bool {
if !pass {
if len(failFormat) != 0 {
var txt string
if str, _, _, _, ok := GetFileAndLineNo(1); ok {
txt = "\n" + str + "> "
}
if len(failArgs) == 0 {
txt += failFormat
} else {
txt += fmt.Sprintf(failFormat, failArgs...)
}
t.Log(txt)
}
t.Fail()
}
return pass
}
Supporting file and line function:
package goUtil
import (
"fmt"
"path/filepath"
"runtime"
)
// GetFileAndLineNo returns information about the file and line in skip levels up the call tree (0 = location this function is called)
func GetFileAndLineNo(skip int) (text string, pc uintptr, file string, line int, ok bool) {
pc, file, line, ok = runtime.Caller(skip + 1)
if ok {
text = fmt.Sprintf("%s:%d", filepath.Base(file), line)
}
return text, pc, file, line, ok
}