Reflection
Runtime types: TypeScript vs Go
Section titled “Runtime types: TypeScript vs Go”In TypeScript you can inspect type metadata at runtime via reflect-metadata and decorators — but this is opt-in, requires experimentalDecorators and emitDecoratorMetadata in tsconfig.json, and the metadata is attached manually by decorator functions. JavaScript erases almost all type information at runtime; decorators are a workaround, not a language primitive.
Go is compiled, but unlike TypeScript it retains full type information at runtime through the reflect package. Every value in Go knows its own type at runtime — no annotations, no build flags needed.
// TypeScript — needs reflect-metadata + decorator compiler flagsimport 'reflect-metadata';
function Column(target: any, key: string) { const type = Reflect.getMetadata('design:type', target, key); console.log(`${key}: ${type.name}`);}
class User { @Column name: string = ''; @Column age: number = 0;}// Go — built-in, no flags neededimport "reflect"
type User struct { Name string Age int}
u := User{Name: "Alice", Age: 30}t := reflect.TypeOf(u)
for i := 0; i < t.NumField(); i++ { f := t.Field(i) fmt.Printf("%s: %s\n", f.Name, f.Type)}// Name: string// Age: intStruct tags
Section titled “Struct tags”Struct tags are string literals embedded in field declarations. They are invisible at compile time and only readable at runtime via reflect. This is how encoding/json, database/sql scanners, and validation libraries like go-playground/validator know which JSON key maps to which field.
// TypeScript — class-validator uses decoratorsimport { IsEmail, IsString, Min } from 'class-validator';
class CreateUserDto { @IsString() name: string;
@IsEmail() email: string;
@Min(0) age: number;}// Go — struct tags (plain strings, read via reflect)type CreateUserDTO struct { Name string `json:"name" validate:"required"` Email string `json:"email" validate:"required,email"` Age int `json:"age" validate:"min=0"`}
// encoding/json uses the "json" tag automatically.// Validator libraries read "validate" at runtime.Reading struct tags at runtime
Section titled “Reading struct tags at runtime”The canonical pattern for reading tags is reflect.Type.Field(i).Tag.Get("key"):
func printJSONTags(v any) { t := reflect.TypeOf(v) if t.Kind() == reflect.Ptr { t = t.Elem() // dereference pointer } for i := 0; i < t.NumField(); i++ { f := t.Field(i) jsonKey := f.Tag.Get("json") fmt.Printf("Field: %-10s json=%q\n", f.Name, jsonKey) }}reflect.Value vs reflect.Type
Section titled “reflect.Value vs reflect.Type”reflect.TypeOf gives you the type descriptor (kind, fields, methods). reflect.ValueOf gives you a boxed value you can inspect and — carefully — mutate.
u := User{Name: "Alice", Age: 30}
// Type introspectiont := reflect.TypeOf(u)fmt.Println(t.Kind()) // structfmt.Println(t.NumField()) // 2
// Value introspectionv := reflect.ValueOf(u)fmt.Println(v.Field(0)) // Alicefmt.Println(v.Field(1)) // 30
// Mutation requires a pointer and CanSet()vp := reflect.ValueOf(&u).Elem()vp.Field(0).SetString("Bob")fmt.Println(u.Name) // BobTry it
Section titled “Try it”package main
import ( "fmt" "reflect")
type User struct { Name string `json:"name" validate:"required"` Email string `json:"email" validate:"required,email"` Age int `json:"age"`}
func printStructTags(v any) { t := reflect.TypeOf(v) if t.Kind() == reflect.Ptr { t = t.Elem() } fmt.Println("--- Struct tags ---") for i := 0; i < t.NumField(); i++ { f := t.Field(i) fmt.Printf("%-8s json=%q validate=%q\n", f.Name, f.Tag.Get("json"), f.Tag.Get("validate"), ) }}
func printValues(v any) { rv := reflect.ValueOf(v) rt := rv.Type() fmt.Println("--- Values ---") for i := 0; i < rv.NumField(); i++ { fmt.Printf("%-8s = %v\n", rt.Field(i).Name, rv.Field(i)) }}
func main() { printStructTags(u) printValues(u)}Loading Go runtime (first run only, ~8 MB)…