Proposal: Add a generic-based compile-time interface assertion helper
TL;DR
Introduce a modern, generic-friendly alternative to Go's traditional interface implementation check.
Current idiom:
var _ SomeInterface = (*SomeStruct)(nil)
Proposed alternative:
var _ = lo.Implement[SomeInterface](&SomeStruct{})
Both provide a compile-time assertion, but the new form aims to be more explicit and idiomatic in the era of generics.
Motivation
Many long-standing Go idioms have gradually been replaced by more modern, expressive forms as the language evolves.
For example, deleting a slice range used to require manual slicing:
a = append(a[:i], a[j:]...)
Now Go encourages a clearer and safer built-in helper:
a = slices.Delete(a, i, j)
However, one particular idiom still carries the “old Go” flavor—the pattern for asserting that a struct implements an interface:
var _ SomeInterface = (*SomeStruct)(nil)
Although widely used, this pattern has some drawbacks:
- It is not immediately intuitive for newcomers.
- It reads more like a trick than an explicit assertion.
- It feels out of place in modern, generic-heavy Go codebases.
Since lo embraces functional-style utilities and code clarity, this proposal explores whether a small generic helper could provide a more expressive alternative.
Proposal
Introduce a generic helper that performs a compile-time interface implementation check:
func Implement[T any](_ T) struct{} {
return struct{}{}
}
Usage:
var _ = Implement[SomeInterface](&SomeStruct{})
Variants
Support asserting multiple implementations at once:
func Implement[T any](_ ...T) struct{} {
return struct{}{}
}
var _ = Implement[SomeInterface](&SomeStruct{}, &AnotherStruct{})
Or a void-return version:
func Implement[T any](_ T) {}
func init() {
Implement[SomeInterface](&SomeStruct{})
}
Why this might fit lo
- It is tiny and almost zero-cost.
- It improves clarity and self-documentation.
- It complements lo’s philosophy of composable helpers.
Why it might not fit lo
lotraditionally focuses on functional utilities, not language-level contracts.- The classic idiom is already short and widely understood.
- The community has not yet standardized a generic-based interface assertion pattern.
Alternative: publish under a smaller module
I understand this proposal is somewhat outside lo’s core domain, but if there’s interest, it could live in a small side package, such as:
lo/assertslo/constraints
Hey @nickxudotme
Thanks for the proposal.
I would suggest something like lo.Cast[MyInterface](&myStruct{}):
func Cast[T any](t T) T {
return t
}
Hey @nickxudotme
Thanks for the proposal.
I would suggest something like
lo.Cast[MyInterface](&myStruct{}):func Cast[T any](t T) T { return t }
Thanks for your reply! Yeah, your version feels cleaner and fits the runtime vibe of lo much better.
If you’re okay with it, I’d love to open a PR adding Cast to type_manipulation.go, plus some comments and a Playground link to match the style of the other functions.
Just let me know! 😄
By the way, I ran a benchmark to confirm that this function incurs no memory allocations after compiler optimizations, making it a suitable replacement for the old idiom.
package main
import (
"testing"
)
type SomeInterface interface {
Foo()
}
type SomeStruct struct{}
func (s *SomeStruct) Foo() {}
type AnotherStruct struct{}
func (s AnotherStruct) Foo() {}
func Cast[T any](t T) T {
return t
}
func BenchmarkCastPointer(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = Cast[SomeInterface](&SomeStruct{})
}
}
func BenchmarkCastValue(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = Cast[SomeInterface](AnotherStruct{})
}
}
Result
goos: darwin
goarch: arm64
pkg: hello
cpu: Apple M3 Max
BenchmarkCastPointer
BenchmarkCastPointer-14 1000000000 0.2761 ns/op 0 B/op 0 allocs/op
BenchmarkCastValue
BenchmarkCastValue-14 1000000000 0.2680 ns/op 0 B/op 0 allocs/op
PASS
Let's discuss implementation before opening the PR.
In #624 I was suggesting different variants:
Cast[T any](v T)-> returns casted value (with compile-time validation)CastOr[T any](v any, fallback T)-> returns the casted value or fallback valueCastOrEmpty[T any](v any, fallback T)-> returns the casted value or empty value
WDYT ?
Hi @nickxudotme, I apologize for intervening in the discussion so abruptly.
It seems that Implement handles two tasks:
- type conversion and
- type assignable assertion.
However, neither the root word nor the part of speech of this verb expresses these meanings. Therefore, I believe a different name is appropriate.
As for how to implement it, I agree with what was mentioned above, but perhaps some stricter distinctions could be made regarding naming semantics.
StaticCast[T any](v T) TorStaticCast[T](... T)with compile-time assertion (naming like C++ https://en.cppreference.com/w/cpp/language/static_cast.html). It also guarantees the assignability of embedded types.Cast[Src, Dst any](src Src) (Dst, bool)orMustCast[Src, Dst any](src Src) Dstwith panic if cast failedCastOr[Src, Dst any](src Src, def Dst) DstorCastOrSupply[Src, Dst any](src Src, f func() Dst) Dstwhich will be called if casting failedCastOrZero[Src, Dst any](src Src) Dstwith zero value of typeDstjust likevar dst Dstdeclared
OK, I am trying to make this perfect.
Thanks @jizhuozhi for the detailed proposal! I really appreciate the thoughtful design.
Let me analyze each function from a practical perspective:
1. StaticCast[T any](v T) T
High usage frequency - this is my original requirement.
// Practical scenario: ensuring a type implements an interface
var _ = StaticCast[io. Writer](&bytes.Buffer{})
var _ = StaticCast[http.Handler](MyHandler{})
However, I think the name Cast is already sufficient.
2. Cast[Src, Dst any](src Src) (Dst, bool)
Very common requirement.
func HandleRequest(data any) error {
user, ok := Cast[any, *User](data)
if !ok {
return errors.New("invalid user data")
}
// use user...
}
But Go's assertion syntax is already concise enough:
user, ok := data.(*User)
user, ok := Cast[any, *User](data)
3. MustCast[Src, Dst any](src Src) Dst
Same as above, we can directly use the native assertion syntax.
4. CastOr[Src, Dst any](src Src, def Dst) Dst
High usage frequency, and indeed more concise than the native approach.
age, ok := data.(int)
if !ok {
age = 0
}
age := CastOr[any, int](data, 0) // Or: CastOr(data, 0)
Or we can use CastOr[Dst any](src any, fallback Dst) Dst , this actually looks better
5. CastOrSupply[Src, Dst any](src Src, f func() Dst) Dst
A lazy-loading variant, but do we really need this in practice?
Unless the default value is very expensive to compute.
I find it hard to imagine scenarios where this is necessary.
6. CastOrZero[Src, Dst any](src Src) Dst
(Also known as CastOrEmpty, but I think CastOrZero is more consistent in style)
Returns zero value variant:
// Practical scenario
count := CastOrZero[any, int](response["count"])
name := CastOrZero[any, string](userMap["name"])
// Equivalent to:
count := CastOr(response["count"], 0)
name := CastOr(userMap["name"], "")
And it has an advantage in a small scenario - for example, 0 is inferred as int by default, but what if you want int64?
count := CastOrZero[any, int64](response["count"]) // Clear
count := CastOr(response["count"], int64(0)) // Requires type conversion
count := CastOr[any, int64](response["count"], 0) // Or this way
Or we can use CastOrZero[Dst any](src any) Dst
Conclusion
So I think we need to provide three functions:
// 1. Compile-time type assertion
func Cast[T any](v T) T {
return v
}
// 2. Runtime type conversion with custom fallback
func CastOr[Src, Dst any](src Src, fallback Dst) Dst {
if v, ok := any(src).(Dst); ok {
return v
}
return fallback
}
// Or
func CastOr[Dst any](src any, fallback Dst) Dst {
if v, ok := src.(Dst); ok {
return v
}
return fallback
}
// 3. Runtime type conversion with zero value fallback
func CastOrZero[Src, Dst any](src Src) Dst {
if v, ok := any(src).(Dst); ok {
return v
}
var zero Dst
return zero
}
// Or
func CastOrZero[Dst any](src any) Dst {
if v, ok := src.(Dst); ok {
return v
}
var zero Dst
return zero
}
Usage examples:
// Scenario 1: Compile-time check - use Cast
var _ = lo.Cast[http.Handler](MyHandler{})
// Scenario 2: Need zero value - use CastOrZero
age := lo.CastOrZero[any, int](userData["age"])
age := lo.CastOrZero[int](userData["age"]) // looks better
// Scenario 3: Need custom default value - use CastOr
age := lo.CastOr[any, int](userData["age"], 18)
age := lo.CastOr[int](userData["age"], 18) // looks better
This is already concise and practical, and can cover most scenarios.