Skip to content

proposal: spec: express pointer/struct/slice/map/array types as possibly-const interface types #28608

@ianlancetaylor

Description

@ianlancetaylor

I propose that it be possible to express pointer, struct, slice, map, and array types as interface types. This issue is an informal description of the idea to see what people think.

Expressing one of these types as an interface type will be written as though the non-interface type were embedded in the interface type, as in type TI interface { T }, or in expanded form as type TS interface { struct { f1 int; f2 string } }. An interface of this form may be used exactly as the embedded type, except that all operations on the type are implemented as method calls on the embedded type. For example, one may write

type S struct {
    f1 int
    f2 string
}

type TS interface {
    S
}

func F(ts TS) int {
    ts.f1++
    return ts.f1 + len(ts.f2)
}

The references ts.f1 and ts.f2 are implemented as method calls on the interface value ts. The methods are implemented as the obvious operations on the underlying type.

The only types that can be converted to TS are structs with the same field names and field types as S (the structs may have other fields as well).

Embedding multiple struct types in an interface type can only be implemented by a struct with all the listed fields. Embedding a struct and a map type, or other combinations, can not be implemented by any type, and is invalid.

For an embedded pointer type, an indirection on the pointer returns an interface value embedding the pointer target type. For example:

type S struct {
    F int
}

type IP interface { *S }
type IS interface { S }

func F(ip IP) {
    s := *ip // s has type IS
    *ip = s // INVALID: s is type IS, but assignment requires type S
    *ip = S{0} // OK
}

Values of one of these interface type may use a type assertion or type switch in the usual way to recover the original value.

This facility in itself is not particularly interesting (it does permit writing an interface type that implements "all structs with a field of name F and type T). To make it more interesting, we add the ability to embed only the getters of the various types, by using const.

type TS interface { const S }

Now the interface type provides only the methods that read fields of S, not the methods that change (or take the address of) fields. This then provides a way to pass a struct (or slice, etc.) to a function without giving the function the ability to change any elements.

Of course, the function can still use a type assertion or type switch to uncover the original value and modify fields that way. Or the function can use the reflect package similarly. So this is not a foolproof mechanism.

Nor should it be. For example, it can be useful to write type ConstByteSlice interface { const []byte } and to use that byte slice without changing it, while still preserving the ability to write f.Write(cbs.([]byte)), relying on the promise of the Write method without any explicit enforcement.

This ability to move back and forth permits adding "const-qualification" on a middle-out basis, without requiring it to be done entirely bottom-up. It also permits adding a mutation at the bottom of a stack of functions using a const interface, without requiring the whole stack to be adjusted, similar to C++ const_cast.

This is not immutability. If the value in the interface is a pointer or slice or map, the pointed-to elements may be changed by other aliases even if they are not changed by the const interface type.

This is, essentially, the ability to say "this function does not modify this aggregate value by accident (though it may modify it on purpose)." This is similar to the C/C++ const qualifier, but expressed as an interface type rather than as a type qualifier.

This is not generics or operator overloading.

One can imagine a number of other ways to adjust the methods attached to such an interface. For example, perhaps there would be a way to drop or replace methods selectively, or add advice to methods. We would have to work out the exact method names (required in any case for type reflection) and provide a way for people to write methods with the same names. That would come much closer to operator overloading, so it may or may not be a good idea.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions