Go Generics in Practice
Record the work and findings when refactoring http://github.com/vincent178/gocsv with generics.
Generics
Go team filed the change proposal to add generics support on 01/12/2021, this is released in version 1.18 on 03/15/2022.
Despite the decision to use [T]
instead of <T>
which is more common in the industry and design with Gcshape instead of Monomorphization which causes discussion around performance downgrade when using generics.
This is still super existing, generics means we could deal with concrete type but remain type restriction and functionality duplication, you know the frustration that deals with interface{}
everywhere and type conversion.
Result
I had a simple tool to parse CSV to a struct, before generics, I have to pass the result as an argument to get the type information:
type Person struct {
Name string `csv:"Name"`
Age uint `csv:"Age"`
Height int `csv:"Height"`
IsTeacher bool `csv:"Is Teacher"`
}
var p Person
goutils.CsvMapToStruct(data, &p)
After refactoring, now it looks like this:
persons, err := gocsv.Read[Person](data) // ✅ persons now have concrete type
I’m pretty satisfied with the result, though it has some limitations which I will elaborate on below.
Tricks
The trick I use to initialize with type parameter:
func p[T any]() T {
var t T // ✅ alloc memory to hold data
// do anything
return t
}
But you can’t do like:
func p[T any]() T {
t := T{} // ❌ compile error: invalid composite literal struct T
// do anything
return t
}
Because T
is not a struct, doing so the compiler will yield an error invalid composite literal struct T
.
Limitations
The Go generics implementation still has some limitations compared to other languages like Rust.
- Type inference
You might notice that we have to specify the type when calling gocsv.Read[Person]
, and you can’t do below either:
var persons []Person
persons, err = gocsv.Read(data)
That’s because Go can’t do type infer based on the return value.
For comparision, Rust don’t need specify type when calling walk
, it can infer type based on return value p:
#[derive(Default)]
struct Person {
name: String,
}
fn walk<T: Default>() -> T {
let t = T::default();
t
}
fn main() {
let p: Person;
p = walk(); // ✅ infer type based on p
println!("{}", p.name);
}
- Any struct
Go provide a handy built-in type parameter any
, but there’s no such type parameter for struct
. In my implementation of gocsv, I have to pass any
as type parameter, and use reflect
doing runtime check on arguments instead of compiler time check.
// func Read[T any](r io.Reader, options ...func(*option)) ([]T, error)
rv := reflect.ValueOf(out)
if rv.Kind() != reflect.Struct {
return nil, errors.New(fmt.Sprintf("invalid generic type %v", rv.Kind()))
}
- Generic method
Go supports generic function, but not allow generics method.
type Person {
Name string
}
func (*Person) Walk[T Animal](t T) {} // ❌ compile error: method must have no type parameter
Go only allow put type parameter in the struct level, which might not be best fit for all the cases, in the above case, the generic type Animal
is only used in Walk
method, and have to move to struct level seems a bit wired.
Again, Rust can do this easily:
#[derive(Default)]
struct Person {
name: String,
}
trait Animal {
fn name(&self) -> String;
}
impl Person {
fn walk<T: Animal>(t: T) {
println!("walk {} now!", t.name())
}
}
Summerize
Generic gives us the way to specify type constraints and work with concrete type instead of interface
, this is super powerful, and I’m looking forward to future improvements in generics.
References
- https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md
- https://go.dev/blog/go1.18
- https://github.com/golang/proposal/blob/master/design/generics-implementation-dictionaries-go1.18.md
- https://en.wikipedia.org/wiki/Monomorphization
- https://planetscale.com/blog/generics-can-make-your-go-code-slower
- https://stackoverflow.com/questions/8263546/whats-the-difference-of-functions-and-methods-in-go