Consider the following program:

package main

import (
    "fmt"
    "unsafe"
)

func indir[T any]() bool {
    var z T
    y := any(z)

    type iface struct {
        itab uintptr
        ptr  unsafe.Pointer
    }

    p := (*iface)(unsafe.Pointer(&y)).ptr
    return p != nil
}

func main() {
    fmt.Println(
        indir[int](),
        indir[*int](),
        indir[map[int]int](),
        indir[struct{ _ *int }](),
        indir[struct {
            _ struct{}
            _ *int
        }](),
    )
}

indir() returns true if some type would need to allocate to be converted to an any. It does so by noting that a freshly created non-nil any's data pointer will never be nil if an allocation happened, but it will be nil if it didn't, because x is all zeros, and in the non-allocating case, this value gets splatted into the data pointer.

This prints true false false false true. However, there is no reason this could not print true false false false false, because the last of these types has the same shape as a pointer, and thus can be passed around as the unsafe.Pointer in an interface value without requiring an allocation, just like is already done for one field structs and arrays of pointer shape.

The change here is essentially applying this to anything that happens to have pointer shape, rather than the current mess of special cases in reflect (notably this excludes struct{*int; struct{}}, which is 16, not 8 bytes wide). Whether something has pointer shape is not visible to users, so this optimization shouldn't break anyone except for very naughty unsafe users like me.

My reason for interest in this is to have the ability to embed zero-size types into a struct, to simplify the implementation of interfaces with large method sets, or embedding e.g. _ [0]sync.Mutex to get govet to do the right thing, a fairly common idiom in concurrency primitives (again, a thing I wind up doing a lot).

I'm not expecting this to get fixed any time soon, because of repercussions across runtime, reflect, and internal/abi, but I wanted to write this up because it's a thorn in my side right now.

Comment From: gabyhelp

Related Issues

(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)

Comment From: randall77

We should be able to fix this. I think we could change cmd/compile/internal/types/type.go:IsDirectIface to return something like t.Size() == int64(PtrSize) && t.ptrBytes == int64(PtrSize).

As you say, there are probably places in reflect at least, that may need to change. The very end of StructOf, maybe others.

Most code, including most of reflect, just uses whatever the compiler decided and wrote down in a runtime type (internal/abi.(*Type).IfaceIndir). Those uses should be fine unchanged.

Comment From: mcy

Actually, this could also be applied to struct{*int; struct{}}, seeing as any operation that extracts a value out of an interface (type assertions and itab thunks) produces a copy that gets passed in registers, so there's no need to store the tail padding...

Although if someone depends on tail padding being preserved they're in for a nasty surprised.

Comment From: gopherbot

Change https://go.dev/cl/681937 mentions this issue: cmd/compile: allow multi-field structs to be stored directly in interfaces