Go测试中defer清理函数调用位置不同引发行为异常
Let's break down what's happening here and fix it:
The Problem Recap
You have a test that inserts base fixtures and then new records, with a deferred cleanup function that only deletes the base fixtures on its first run. Only when you add a second deferred call after inserting new records does cleanup work correctly. You suspect defer's parameter evaluation is involved, but are confused since you're passing pointers.
Your Original Code Snippets
Test Function:
var testInstance aetest.Instance // initialized by a TestMain func TestThing(t *testing.T) { defer cleanupGoogleDatastore(t, testInstance) // insert basic test fixtures // insert some new records here // test assertions, etc. }
Cleanup Function:
func cleanupGoogleDatastore(t *testing.T, testInstance aetest.Instance) { q := datastore.NewQuery("Order") ctx := GetContext(t, testInstance) scanner := q.Run(ctx) for { var o model.Order key, err := scanner.Next(&o) if err == datastore.Done { return } if err != nil { t.Fatal(err.Error()) } err = datastore.Delete(ctx, key) if err != nil { t.Fatal(err.Error()) } } }
Root Cause: Value-Type Parameter in Defer
The key issue here is how Go's defer works combined with the type of testInstance:
- Go evaluates all arguments to a deferred function at the moment the
deferstatement is declared, not when the function runs later. - Your
testInstanceis declared as a value type (aetest.Instance) rather than a pointer. When you pass it tocleanupGoogleDatastorein the firstdeferstatement, Go makes a full copy of the instance's value right then. - If inserting records modifies the internal state of the global
testInstance(common in App Engine test instances, e.g., tracking session context or transaction state), the deferred cleanup function is still holding onto the original copy of the instance from before you inserted new records. This means the context created from that stale copy can't see the newly inserted data. - When you add a second
deferafter inserting records, it captures the updatedtestInstancevalue, so its cleanup can see and delete the new records.
The Fix: Use a Pointer for testInstance
Change your global testInstance to a pointer type, and update the cleanup function to accept a pointer:
Updated Test Setup:
var testInstance *aetest.Instance // Now a pointer // Initialize it in TestMain with testInstance, err := aetest.NewInstance(...) func TestThing(t *testing.T) { defer cleanupGoogleDatastore(t, testInstance) // insert basic test fixtures // insert some new records here // test assertions, etc. }
Updated Cleanup Function:
func cleanupGoogleDatastore(t *testing.T, testInstance *aetest.Instance) { // Accept pointer q := datastore.NewQuery("Order") ctx := GetContext(t, *testInstance) // Dereference if needed by GetContext scanner := q.Run(ctx) for { var o model.Order key, err := scanner.Next(&o) if err == datastore.Done { return } if err != nil { t.Fatal(err.Error()) } err = datastore.Delete(ctx, key) if err != nil { t.Fatal(err.Error()) } } }
Why This Works
- Pointers in Go are reference types. When you pass
testInstance(a pointer) to the deferred function, Go copies the pointer's value (the memory address of the actual instance) at defer declaration time. - Even if the internal state of the instance changes later (from inserting records), the pointer still points to the same underlying instance. When the cleanup runs, it uses the current state of that instance to create a context that can see all records inserted during the test.
- Now you only need one
defercall at the start of your test, and it will clean up both base fixtures and new records.
内容的提问来源于stack exchange,提问作者Venantius

