Why []T Won’t Pass as []interface{}
Go programmers frequently hit a wall when trying to pass a slice of a concrete type—say []int64—into a function that expects []interface{} or []fmt.Stringer. The compiler rejects it outright, and the reason is not a language limitation but a fundamental mismatch in how these two slice types are represented in memory.
The Official Stance
The Go FAQ puts it succinctly: "The two types do not have the same representation in memory." You cannot directly convert []T to []interface{}; you must copy each element individually.
The Go Wiki adds more detail. A []interface{} is not itself an interface—it's a slice whose element type happens to be interface{}. Each interface{} value occupies two machine words on a 64-bit system: one word for the type information, and one for the data or a pointer to it. Consequently, a []interface{} of length N is backed by N*2 words of data. A []MyType of the same length is backed by N*sizeof(MyType) words. The underlying buffers simply look different, so a quick assignment is impossible.
The Invariance Problem
There is a more theoretical angle. In subtyping terms, if type T implements interface I, you might expect []T to be assignable to []I—that is, you might expect slices to be covariant. Go slices are not; they are invariant.
This is not an oversight. Covariance combined with mutability leads to unsound behavior, as demonstrated by Java's covariant arrays. Consider valid Go code that assigns any type to an interface{}:
func foo(ii []interface{}) {
ii[1] = "joe"
}
If slices were covariant, you could pass a []int64 to a function that writes an arbitrary interface{} into the slice:
ints := []int64{1, 2, 3}
foo(ints) // oops!
If that were legal, the function could assign a string into what is really an []int64, causing obscure runtime errors—exactly what statically typed languages try to prevent.
You might argue the problem only exists if the parameter is mutable. Go, however, has no const concept like C++, and even a read-only parameter would still encounter the memory layout issue.
The Memory Layout, In Detail
A Go slice is a header of three contiguous quadwords: a pointer to the backing data array, the length, and the capacity. The backing array holds elements contiguously. For a slice of four int64s, the data occupies four quadwords:
is := []int64{0x55, 0x22, 0xab, 0x9}
If the element type were a struct with three int64s, each element would occupy three quadwords, and the data region would span 12 quadwords total. Layout is strictly tied to the element type.
Now consider []interface{}. An interface{} value holds two pointers: one to an itable (the method dispatch table) and one to the underlying data. Each element therefore takes up two quadwords. Copying four int64s into an []interface{} produces a data region of eight quadwords:
iis := make([]interface{}, len(is))
for i := 0; i < len(is); i++ {
iis[i] = is[i]
}
Each interface element's first quadword points to the itable; the second points to the actual int64 value. Code written to iterate over []interface{} would make a mess if handed an []int64 because the backing memory is entirely different.
Seeing It at Runtime
Memory dumps make the distinction concrete. Using Delve on a small program that builds both is and iis:
var sum int64
func addUpDirect(s []int64) {
for i := 0; i < len(s); i++ {
sum += s[i]
}
}
func addUpViaInterface(s []interface{}) {
for i := 0; i < len(s); i++ {
sum += s[i].(int64)
}
}
func main() {
is := []int64{0x55, 0x22, 0xab, 0x9}
addUpDirect(is)
iis := make([]interface{}, len(is))
for i := 0; i < len(is); i++ {
iis[i] = is[i]
}
addUpViaInterface(iis)
}
Inspecting the slice header for &is:
$ dlv debug slice-layout.go
Type 'help' for list of commands.
(dlv) break slice-layout.go:35
Breakpoint 1 set at 0x467cb5 for main.main() ./slice-layout.go:35
(dlv) c
> main.main() ./slice-layout.go:35 (hits goroutine(1):1 total:1) (PC: 0x467cb5)
30: iis := make([]interface{}, len(is))
31: for i := 0; i < len(is); i++ {
32: iis[i] = is[i]
33: }
34:
=> 35: addUpViaInterface(iis)
36: }
(dlv) p &is
(*[]int64)(0xc00003c748)
(dlv) x -fmt hex -len 32 0xc00003c748 0xc00003c748: 0x18 0xc7 0x03 0x00 0xc0 0x00 0x00 0x00 0xc00003c750: 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0xc00003c758: 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0xc00003c760: 0x00 0xe0 0x10 0x00 0xc0 0x00 0x00 0x00
Dumping the data region shows the four int64 values, one per quadword:
(dlv) x -fmt hex -len 32 0xc00003c718 0xc00003c718: 0x55 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0xc00003c720: 0x22 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0xc00003c728: 0xab 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0xc00003c730: 0x09 0x00 0x00 0x00 0x00 0x00 0x00 0x00
For iis, the header shows the same length and capacity, but the data region is different:
(dlv) p &iis
(*[]interface {})(0xc00003c760)
(dlv) x -fmt hex -len 32 0xc00003c760
0xc00003c760: 0x00 0xe0 0x10 0x00 0xc0 0x00 0x00 0x00
0xc00003c768: 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0xc00003c770: 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0xc00003c778: 0xd0 0xc7 0x03 0x00 0xc0 0x00 0x00 0x00
(dlv) x -fmt hex -len 64 0xc00010e000 0xc00010e000: 0xe0 0xf1 0x46 0x00 0x00 0x00 0x00 0x00 0xc00010e008: 0xa8 0x44 0x4c 0x00 0x00 0x00 0x00 0x00 0xc00010e010: 0xe0 0xf1 0x46 0x00 0x00 0x00 0x00 0x00 0xc00010e018: 0x10 0x43 0x4c 0x00 0x00 0x00 0x00 0x00 0xc00010e020: 0xe0 0xf1 0x46 0x00 0x00 0x00 0x00 0x00 0xc00010e028: 0x58 0x47 0x4c 0x00 0x00 0x00 0x00 0x00 0xc00010e030: 0xe0 0xf1 0x46 0x00 0x00 0x00 0x00 0x00 0xc00010e038: 0x48 0x42 0x4c 0x00 0x00 0x00 0x00 0x00
The even-numbered quadwords all hold the same address—the itable for interface{}. The odd quadwords point to the actual data values:
(dlv) x -fmt hex -len 8 0x4c44a8 0x4c44a8: 0x55 0x00 0x00 0x00 0x00 0x00 0x00 0x00 (dlv) x -fmt hex -len 8 0x4c4310 0x4c4310: 0x22 0x00 0x00 0x00 0x00 0x00 0x00 0x00 (dlv) x -fmt hex -len 8 0x4c4758 0x4c4758: 0xab 0x00 0x00 0x00 0x00 0x00 0x00 0x00 (dlv) x -fmt hex -len 8 0x4c4248 0x4c4248: 0x09 0x00 0x00 0x00 0x00 0x00 0x00 0x00
Generated Code Is Different Too
The compiler also produces markedly different code for these two slice types, as disassembling the relevant functions shows.
For a direct []int64 access, the loop is simple—indexing into the linearly laid-out quadwords:
"".addUpDirect STEXT nosplit size=41 args=0x18 locals=0x0 0x0000 00000 (slice-layout.go:11) TEXT "".addUpDirect(SB) 0x0000 00000 (slice-layout.go:12) MOVQ "".s+16(SP), AX 0x0005 00005 (slice-layout.go:12) MOVQ "".s+8(SP), CX 0x000a 00010 (slice-layout.go:12) XORL DX, DX 0x000c 00012 (slice-layout.go:12) JMP 35 0x000e 00014 (slice-layout.go:13) MOVQ "".sum(SB), BX 0x0015 00021 (slice-layout.go:13) ADDQ (CX)(DX*8), BX 0x0019 00025 (slice-layout.go:13) MOVQ BX, "".sum(SB) 0x0020 00032 (slice-layout.go:12) INCQ DX 0x0023 00035 (slice-layout.go:12) CMPQ DX, AX 0x0026 00038 (slice-layout.go:12) JLT 14 0x0028 00040 (slice-layout.go:12) RET
For []interface{}, the generated code is more complex. It must account for the two-word interface representation. The compiler uses an instruction like SHLQ $4, DX to multiply the loop index by 16 bytes per element, then accesses the second quadword (the data pointer) via an offset of 8 bytes:
"".addUpViaInterface STEXT size=150 args=0x18 locals=0x20
0x0000 00000 (slice-layout.go:19) TEXT "".addUpViaInterface(SB)
0x0000 00000 (slice-layout.go:19) MOVQ (TLS), CX
0x0009 00009 (slice-layout.go:19) CMPQ SP, 16(CX)
0x000d 00013 (slice-layout.go:19) JLS 140
0x000f 00015 (slice-layout.go:19) SUBQ $32, SP
0x0013 00019 (slice-layout.go:19) MOVQ BP, 24(SP)
0x0018 00024 (slice-layout.go:19) LEAQ 24(SP), BP
0x001d 00029 (slice-layout.go:20) MOVQ "".s+48(SP), AX
0x0022 00034 (slice-layout.go:20) MOVQ "".s+40(SP), CX
0x0027 00039 (slice-layout.go:20) XORL DX, DX
0x0029 00041 (slice-layout.go:20) JMP 64
0x002b 00043 (slice-layout.go:21) MOVQ "".sum(SB), SI
0x0032 00050 (slice-layout.go:21) ADDQ (DX), SI
0x0035 00053 (slice-layout.go:21) MOVQ SI, "".sum(SB)
0x003c 00060 (slice-layout.go:20) LEAQ 1(BX), DX
0x0040 00064 (slice-layout.go:20) CMPQ DX, AX
0x0043 00067 (slice-layout.go:20) JGE 103
0x0045 00069 (slice-layout.go:21) MOVQ DX, BX
0x0048 00072 (slice-layout.go:21) SHLQ $4, DX
0x004c 00076 (slice-layout.go:21) MOVQ (CX)(DX*1), SI
0x0050 00080 (slice-layout.go:21) MOVQ 8(CX)(DX*1), DX
0x0055 00085 (slice-layout.go:21) LEAQ type.int64(SB), DI
0x005c 00092 (slice-layout.go:21) NOP
0x0060 00096 (slice-layout.go:21) CMPQ DI, SI
0x0063 00099 (slice-layout.go:21) JEQ 43
0x0065 00101 (slice-layout.go:21) JMP 113
0x0067 00103 (slice-layout.go:20) MOVQ 24(SP), BP
0x006c 00108 (slice-layout.go:20) ADDQ $32, SP
0x0070 00112 (slice-layout.go:20) RET
0x0071 00113 (slice-layout.go:21) MOVQ SI, (SP)
0x0075 00117 (slice-layout.go:21) MOVQ DI, 8(SP)
0x007a 00122 (slice-layout.go:21) LEAQ type.interface {}(SB), AX
0x0081 00129 (slice-layout.go:21) MOVQ AX, 16(SP)
0x0086 00134 (slice-layout.go:21) CALL runtime.panicdottypeE(SB)
0x008b 00139 (slice-layout.go:21) XCHGL AX, AX
0x008c 00140 (slice-layout.go:21) NOP
0x008c 00140 (slice-layout.go:19) CALL runtime.morestack_noctxt(SB)
Whether you read the assembly in detail, the takeaway is the same: the compiler's access patterns for []int64 and []interface{} are completely different. Passing the wrong slice type to compiled code would cause invalid memory access and crashes. This is not a theoretical concern; it is baked into the generated machine code.
A Path Forward With Generics
The practical need—one function operating on slices of many types—remains. The workaround is not type conversion tricks but generics, which allow writing a single function that works across concrete slice types without sacrificing type safety or memory layout correctness.



