Skip to content

Commit 44848b1

Browse files
jquirkeclaude
andcommitted
Restore callBackToMethodValue function to fix dead code elimination (closes #7622)
Commit 725aa5b replaced the specialized callBackToMethodValue() function with MethodByName(string(cbName)) to fix data race issues, but this inadvertently broke dead code elimination optimization. The original function used explicit string constants in MethodByName calls, which allows the Go compiler/linker to perform dead code elimination. Using MethodByName with a variable parameter prevents this optimization, potentially increasing binary size for large projects. This restores the callBackToMethodValue function while preserving the data race fixes from commit 725aa5b, ensuring both thread safety and optimal binary size for enterprise customers building large binaries. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent b881483 commit 44848b1

File tree

1 file changed

+57
-1
lines changed

1 file changed

+57
-1
lines changed

schema/schema.go

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ func ParseWithSpecialTableName(dest interface{}, cacheStore *sync.Map, namer Nam
353353
}()
354354

355355
for _, cbName := range callbackTypes {
356-
if methodValue := modelValue.MethodByName(string(cbName)); methodValue.IsValid() {
356+
if methodValue := callBackToMethodValue(modelValue, cbName); methodValue.IsValid() {
357357
switch methodValue.Type().String() {
358358
case "func(*gorm.DB) error":
359359
expectedPkgPath := path.Dir(reflect.TypeOf(schema).Elem().PkgPath())
@@ -379,6 +379,62 @@ func ParseWithSpecialTableName(dest interface{}, cacheStore *sync.Map, namer Nam
379379
return schema, schema.err
380380
}
381381

382+
// CRITICAL: This explicit method unrolling is required for dead code elimination.
383+
// DO NOT replace with MethodByName(string(cbType)) or similar dynamic calls.
384+
// If you don't understand why this matters, DO NOT MODIFY this function.
385+
//
386+
// The explicit string constants in each MethodByName call allow the Go compiler
387+
// and linker to determine exactly which methods might be called, enabling dead
388+
// code elimination. Using MethodByName with a variable parameter breaks this
389+
// optimization and can significantly increase binary size for large projects.
390+
//
391+
// Prior to Go 1.22, ANY use of MethodByName with non-constant strings would
392+
// cause the linker to abandon dead code elimination for the ENTIRE binary.
393+
// Go 1.22+ supports the special case of string constants, but variables still
394+
// break the optimization.
395+
//
396+
// For context and technical details, see:
397+
// - https://github.com/golang/go/issues/62257
398+
// - https://docs.google.com/document/d/1KtJSpRvoHddya-P329ZgLwkv-NkyCXCRMX3FOAeDoBk/edit#heading=h.ltobgcan4ct3
399+
func callBackToMethodValue(modelType reflect.Value, cbType callbackType) reflect.Value {
400+
// Split into logical groups to reduce cyclomatic complexity
401+
if method := getCRUDCallbackMethod(modelType, cbType); method.IsValid() {
402+
return method
403+
}
404+
return getLifecycleCallbackMethod(modelType, cbType)
405+
}
406+
407+
func getCRUDCallbackMethod(modelType reflect.Value, cbType callbackType) reflect.Value {
408+
switch cbType {
409+
case callbackTypeBeforeCreate:
410+
return modelType.MethodByName("BeforeCreate")
411+
case callbackTypeAfterCreate:
412+
return modelType.MethodByName("AfterCreate")
413+
case callbackTypeBeforeUpdate:
414+
return modelType.MethodByName("BeforeUpdate")
415+
case callbackTypeAfterUpdate:
416+
return modelType.MethodByName("AfterUpdate")
417+
case callbackTypeBeforeDelete:
418+
return modelType.MethodByName("BeforeDelete")
419+
case callbackTypeAfterDelete:
420+
return modelType.MethodByName("AfterDelete")
421+
}
422+
return reflect.Value{}
423+
}
424+
425+
func getLifecycleCallbackMethod(modelType reflect.Value, cbType callbackType) reflect.Value {
426+
switch cbType {
427+
case callbackTypeBeforeSave:
428+
return modelType.MethodByName("BeforeSave")
429+
case callbackTypeAfterSave:
430+
return modelType.MethodByName("AfterSave")
431+
case callbackTypeAfterFind:
432+
return modelType.MethodByName("AfterFind")
433+
default:
434+
return reflect.ValueOf(nil)
435+
}
436+
}
437+
382438
func getOrParse(dest interface{}, cacheStore *sync.Map, namer Namer) (*Schema, error) {
383439
modelType := reflect.ValueOf(dest).Type()
384440

0 commit comments

Comments
 (0)