Types In Go - Complete Guide By Aman Gupta.

Full Stack Developer specializing in sleek, performant frontends and scalable backend systems I build production ready web applications with a focus on scalability, performance, and clean architecture. My expertise spans modern frontend development with React.js , Next.js and TypeScript, combined with robust backend systems.
On the backend, I specialize in Golang microservices using Fiber framework, implementing event-driven architectures with Kafka, caching strategies with Redis, and building efficient APIs with gRPC. I focus on creating scalable, maintainable systems that handle real-world complexity.
From Beginner to Interview Ready
Part 1: Foundation (Beginner Level)
Understanding Types - What and Why?
A type defines:
What kind of data a variable can hold
What operations you can perform on it
How much memory it uses
Think of types like containers:
An
intcontainer holds whole numbersA
stringcontainer holds textYou can't put text in an
intcontainer!
1. Basic (Built-in) Types
These come with Go by default.
Integers (Whole Numbers)
Used for numbers without decimals.
int, int8, int16, int32, int64 // Can be negative
uint, uint8, uint16, uint32, uint64 // Only positive (unsigned)
Example:
var age int = 25
var temperature int8 = -10 // Range: -128 to 127
var score uint8 = 255 // Range: 0 to 255
Size matters:
var small int8 = 127 // Max value
// var tooBig int8 = 128 // β ERROR - doesn't fit!
Floating-Point Numbers (Decimals)
Used for numbers with decimal points.
float32, float64
Example:
var price float64 = 99.99
var temperature float32 = 36.6
var pi float64 = 3.14159265359
Boolean (True/False)
Only two possible values.
bool
Example:
var isActive bool = true
var hasError bool = false
var isAdult bool = age >= 18 // Result of comparison
String (Text)
Used for text. Strings are immutable (cannot be changed after creation).
string
Example:
var name string = "Alice"
var message string = "Hello, World!"
var empty string = "" // Empty string
Important: Strings cannot be changed:
name := "Alice"
// name[0] = 'B' // β ERROR - strings are immutable
Complex Numbers (Rarely Used)
For mathematical/scientific calculations.
complex64, complex128
Example:
c := complex(2, 3) // 2 + 3i
fmt.Println(c) // (2+3i)
2. The Zero Value Concept
Every type has a default value when declared but not initialized.
var x int // 0
var f float64 // 0.0
var s string // "" (empty string)
var b bool // false
var p *int // nil
Complete table:
| Type | Zero Value |
| int, int8, int16... | 0 |
| float32, float64 | 0.0 |
| bool | false |
| string | "" (empty) |
| pointer | nil |
| slice | nil |
| map | nil |
| channel | nil |
| interface | nil |
| function | nil |
Example:
var count int
fmt.Println(count) // 0 (not random garbage!)
var name string
fmt.Println(name) // "" (not random characters!)
3. Composite Types (Building Blocks)
Built using basic types.
Array (Fixed Size)
A numbered list of elements. Size cannot change.
[size]Type
Example:
var arr [3]int // Array of 3 integers
arr[0] = 10
arr[1] = 20
arr[2] = 30
fmt.Println(arr) // [10 20 30]
// Or initialize directly:
numbers := [3]int{1, 2, 3}
fmt.Println(len(numbers)) // 3
// Cannot add more elements!
// numbers[3] = 4 // β ERROR - index out of range
Important: Size is part of the type!
var a [3]int
var b [5]int
// They are DIFFERENT types! Cannot assign one to another.
Slice (Dynamic Size)
Like an array but can grow and shrink. Most commonly used.
[]Type
Example:
var nums []int // Slice (not array!)
nums = append(nums, 1) // Add element
nums = append(nums, 2, 3) // Add multiple
fmt.Println(nums) // [1 2 3]
// Or initialize directly:
fruits := []string{"apple", "banana"}
fruits = append(fruits, "orange")
fmt.Println(fruits) // [apple banana orange]
Array vs Slice:
// Array - Fixed size
arr := [3]int{1, 2, 3}
// Cannot grow or shrink
// Slice - Dynamic
slice := []int{1, 2, 3}
slice = append(slice, 4) // β
Can grow!
Map (Key-Value Pairs)
Like a dictionary - look up values by keys.
map[KeyType]ValueType
Example:
// Must initialize before use!
ages := make(map[string]int)
// Add entries
ages["Alice"] = 25
ages["Bob"] = 30
// Get value
fmt.Println(ages["Alice"]) // 25
// Check if key exists
age, exists := ages["Charlie"]
if exists {
fmt.Println(age)
} else {
fmt.Println("Not found") // This prints
}
// Initialize with values:
scores := map[string]int{
"Alice": 95,
"Bob": 87,
}
Common mistake:
var m map[string]int // nil map
// m["key"] = 10 // π₯ PANIC! Cannot write to nil map
// Must initialize first:
m = make(map[string]int)
m["key"] = 10 // β
Now it works
Struct (Group Related Data)
Group different pieces of data together.
type Name struct {
field1 Type1
field2 Type2
}
Example:
type Person struct {
Name string
Age int
City string
}
// Create a person
alice := Person{
Name: "Alice",
Age: 25,
City: "NYC",
}
// Access fields
fmt.Println(alice.Name) // Alice
alice.Age = 26 // Modify field
fmt.Println(alice.Age) // 26
// Short form (must follow field order):
bob := Person{"Bob", 30, "LA"}
4. Pointer Type
Stores the memory address of another variable.
*Type // Pointer to Type
Think of it like:
Variable = a house
Pointer = the house's address
Example:
x := 10
fmt.Println(x) // 10
p := &x // & gets the address
fmt.Println(p) // 0xc0000... (memory address)
fmt.Println(*p) // 10 (value at that address)
*p = 20 // Change value through pointer
fmt.Println(x) // 20 (x changed!)
Why use pointers?
- Modify values in functions:
func incrementCopy(x int) {
x = x + 1 // Only changes the copy
}
func incrementOriginal(x *int) {
*x = *x + 1 // Changes the original!
}
num := 5
incrementCopy(num)
fmt.Println(num) // 5 (unchanged)
incrementOriginal(&num)
fmt.Println(num) // 6 (changed!)
- Avoid copying large structs:
type BigData struct {
data [1000000]int
}
// Without pointer - copies entire struct (slow!)
func processCopy(b BigData) {
// ...
}
// With pointer - only passes address (fast!)
func processPointer(b *BigData) {
// ...
}
5. Function Type
Functions are values in Go!
func(parameters) returnType
Example:
// Declare a function variable
var add func(int, int) int
// Assign a function
add = func(a, b int) int {
return a + b
}
result := add(3, 5) // 8
// Pass function as parameter
func calculate(operation func(int, int) int, x, y int) int {
return operation(x, y)
}
sum := calculate(add, 10, 20) // 30
6. Interface Type
Defines behavior (what methods a type should have).
type InterfaceName interface {
MethodName(parameters) returnType
}
Simple example:
type Speaker interface {
Speak() string
}
type Dog struct {
Name string
}
func (d Dog) Speak() string {
return "Woof!"
}
type Cat struct {
Name string
}
func (c Cat) Speak() string {
return "Meow!"
}
// Both Dog and Cat implement Speaker
func makeSound(s Speaker) {
fmt.Println(s.Speak())
}
dog := Dog{Name: "Buddy"}
cat := Cat{Name: "Whiskers"}
makeSound(dog) // Woof!
makeSound(cat) // Meow!
Empty Interface
interface{} or any - accepts any type.
var x interface{}
x = 10 // β
int
x = "hello" // β
string
x = []int{1, 2} // β
slice
x = true // β
bool
// Anything can be stored!
Common uses:
fmt.Println(x)- accepts any typeJSON decoding - unknown structure
Generic containers
Important: Must use type assertion to get value back:
var x interface{} = "Hello"
// s := x + " World" // β ERROR - x is interface{}, not string
s := x.(string) + " World" // β
Type assertion
fmt.Println(s) // Hello World
7. Channel Type
For communication between goroutines (concurrent functions).
chan Type
Basic example:
ch := make(chan int)
// Send value (in a goroutine)
go func() {
ch <- 42 // Send 42 to channel
}()
// Receive value
value := <-ch
fmt.Println(value) // 42
Types of channels:
chan int // Can send and receive
chan<- int // Can only send
<-chan int // Can only receive
8. Custom (Named) Types
Create your own types based on existing ones.
Simple Named Type
type TypeName BaseType
Example:
type Age int
type Email string
type Score float64
var myAge Age = 25
var myEmail Email = "alice@example.com"
Why create named types?
- Prevent mixing values:
type Celsius float64
type Fahrenheit float64
var c Celsius = 25.0
var f Fahrenheit = 77.0
// c = f // β ERROR - Different types!
// Prevents accidentally mixing temperature units
- Add methods:
type Age int
func (a Age) IsAdult() bool {
return a >= 18
}
myAge := Age(20)
fmt.Println(myAge.IsAdult()) // true
- Better code clarity:
type UserID int
type ProductID int
func getUser(id UserID) { /* ... */ }
func getProduct(id ProductID) { /* ... */ }
// Clear what each function expects!
Named type is a NEW type:
type MyInt int
var a MyInt = 10
var b int = 10
// a = b // β ERROR - Different types!
a = MyInt(b) // β
Must convert
When to Use Struct vs Simple Type?
Single value β Simple type:
type Age int
type Email string
Multiple values β Struct:
type Person struct {
Name string
Age int
Email string
}
9. Type Alias
Same type, just a different name. No new type created.
type NewName = ExistingType
Example:
type MyInt = int // Alias
var a MyInt = 10
var b int = 10
a = b // β
Works! They're the same type
Named Type vs Alias:
// Named Type - Creates NEW type
type Celsius float64
var c Celsius = 25.0
var f float64 = 25.0
// c = f // β ERROR - Different types
// Alias - Same type, different name
type Temperature = float64
var t Temperature = 25.0
var x float64 = 25.0
t = x // β
Works - Same type!
When to use:
Named type: Want type safety, add methods
Alias: Renaming for clarity, backward compatibility
10. Special Values
nil
Zero value for reference types.
var p *int // nil
var s []int // nil
var m map[string]int // nil
var ch chan int // nil
var i interface{} // nil
var f func() // nil
nil slice vs empty slice:
var s1 []int // nil slice
s2 := []int{} // empty slice (not nil)
fmt.Println(s1 == nil) // true
fmt.Println(s2 == nil) // false
fmt.Println(len(s1)) // 0
fmt.Println(len(s2)) // 0
any
Same as interface{} (introduced in Go 1.18).
var x any = 100 // Same as: var x interface{} = 100
var y any = "text"
Part 2: Type Conversion (Intermediate)
What is Type Conversion?
Changing a value from one type to another.
Go's strict rule: Types never convert automatically!
var a int = 10
var b float64 = 5.5
// c := a + b // β ERROR - Cannot mix types!
c := float64(a) + b // β
Must convert explicitly
fmt.Println(c) // 15.5
Why Go Requires Explicit Conversion
Type safety and clarity. You always know exactly what's happening.
var discount int = 10
var price float64 = 99.99
// total := price - discount // β Caught at compile time!
// Forces you to be clear:
total := price - float64(discount) // β
Explicit intention
Basic Conversion Syntax
Always: NewType(value)
Integer β Float
// Integer to Float
var x int = 20
var y float64 = float64(x)
fmt.Println(y) // 20.0
// Float to Integer (TRUNCATES, doesn't round!)
var a float64 = 9.9
var b int = int(a)
fmt.Println(b) // 9 (not 10!)
var c float64 = 9.1
var d int = int(c)
fmt.Println(d) // 9
Important: Truncation, not rounding!
int(9.9) // 9
int(9.1) // 9
int(-9.9) // -9 (not -10)
Different Integer Sizes
var big int64 = 100
var small int32 = int32(big)
fmt.Println(small) // 100
// Be careful with overflow!
var huge int64 = 999999999999
var tiny int8 = int8(huge)
fmt.Println(tiny) // -1 (WRONG! Overflow occurred)
String β Number Conversion
Cannot use direct conversion! Must use strconv package.
String to Number
import "strconv"
// String to Integer
s := "123"
n, err := strconv.Atoi(s) // ASCII to Integer
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println(n) // 123 (as integer)
}
// String to Float
f, err := strconv.ParseFloat("3.14", 64)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println(f) // 3.14
}
Number to String
// Integer to String
num := 100
str := strconv.Itoa(num) // Integer to ASCII
fmt.Println(str) // "100" (as string)
// Float to String
f := 3.14
s := strconv.FormatFloat(f, 'f', 2, 64) // 2 decimal places
fmt.Println(s) // "3.14"
Common Mistake!
// WRONG: Converts to CHARACTER, not string representation
x := 65
s := string(x)
fmt.Println(s) // "A" (ASCII 65 = 'A'), NOT "65"!
// RIGHT: Use strconv
s := strconv.Itoa(x)
fmt.Println(s) // "65" β
String β Bytes and Runes
Understanding Bytes and Runes
byte= alias foruint8(0-255) - single byterune= alias forint32- Unicode character
String to Bytes/Runes
// String to Bytes
s := "Go"
b := []byte(s)
fmt.Println(b) // [71 111] (ASCII values)
// Bytes to String
bytes := []byte{71, 111}
str := string(bytes)
fmt.Println(str) // "Go"
// String to Runes (for Unicode)
s := "Hello δΈη"
r := []rune(s)
fmt.Println(len(s)) // 12 (bytes)
fmt.Println(len(r)) // 8 (characters)
Byte/Rune to String
// Converts to CHARACTER
var b byte = 65
fmt.Println(string(b)) // "A"
var r rune = 9731
fmt.Println(string(r)) // "β" (snowman)
// Remember:
var x int = 65
string(x) // "A" - character
strconv.Itoa(x) // "65" - string representation
Conversion Rules Summary
β
Always explicit - Go never converts automatically
β
Syntax: Type(value)
β
Float to int - truncates (doesn't round)
β
String β number - use strconv package
β
string(number) - gives character, not representation
β
Watch for overflow with smaller types
Part 3: Type Assertion (Intermediate)
What is Type Assertion?
Used only with interfaces to extract the concrete type inside.
Think of it:
Interface = a box
Type assertion = opening the box to get the value
Type Conversion vs Type Assertion
Completely different operations!
| Aspect | Type Conversion | Type Assertion |
| Works with | Concrete types | Interfaces only |
| Syntax | Type(value) | value.(Type) |
| When | Compile time | Runtime |
| Can fail? | Overflow only | Yes, can panic! |
Examples:
// Type Conversion
var a int = 5
var b float64 = float64(a) // Convert int to float64
// Type Assertion
var x interface{} = 5
y := x.(int) // Extract int from interface
Type Assertion Syntax
Unsafe Way (Can Panic!)
var x interface{} = 10
v := x.(int)
fmt.Println(v) // 10
// But if wrong type:
var y interface{} = "hello"
n := y.(int) // π₯ PANIC! Program crashes!
Safe Way (Always Use This!)
var x interface{} = 10
v, ok := x.(int)
if ok {
fmt.Println("Success:", v) // 10
} else {
fmt.Println("Not an int")
}
// When wrong type:
var y interface{} = "hello"
n, ok := y.(int)
fmt.Println(ok) // false
fmt.Println(n) // 0 (zero value)
// No crash! β
Type Switch
Check multiple types cleanly.
Without type switch (messy):
func describe(v interface{}) {
if _, ok := v.(int); ok {
fmt.Println("It's an int")
} else if _, ok := v.(string); ok {
fmt.Println("It's a string")
} else {
fmt.Println("Unknown")
}
}
With type switch (clean):
func describe(v interface{}) {
switch val := v.(type) { // Note: v.(type) only in switch!
case int:
fmt.Println("Integer:", val*2)
case string:
fmt.Println("String:", val+"!")
case []int:
fmt.Println("Slice, length:", len(val))
case bool:
fmt.Println("Boolean:", val)
default:
fmt.Println("Unknown type")
}
}
describe(42) // Integer: 84
describe("Go") // String: Go!
describe([]int{1, 2}) // Slice, length: 2
describe(3.14) // Unknown type
Key points:
valgets actual value with correct typeNo assertions needed inside cases
typekeyword only works in switch
Real-World Example
// Processing API responses
func handleResponse(response interface{}) {
switch data := response.(type) {
case map[string]interface{}:
fmt.Println("JSON object, keys:", len(data))
case []interface{}:
fmt.Println("JSON array, length:", len(data))
case string:
fmt.Println("Error message:", data)
case nil:
fmt.Println("No data")
default:
fmt.Println("Unexpected type")
}
}
Type Assertion Summary
β
Only for interfaces - not for concrete types
β
Always use safe form: value, ok := x.(Type)
β
Type switch for multiple types
β
Runtime operation - can fail
β
Different from conversion - extraction, not transformation
Part 4: Advanced Concepts
1. No Implicit Type Conversion
Go's golden rule: Types never change automatically.
var a int = 10
var b float64 = 10.0
// fmt.Println(a == b) // β ERROR - Different types!
fmt.Println(float64(a) == b) // β
Must convert
Why this matters:
Prevents hidden bugs
Makes code explicit
Always know exact types
Prevented bug example:
type UserID int
type ProductID int
func getUser(id UserID) { /* ... */ }
var pid ProductID = 123
// getUser(pid) // β ERROR - Caught at compile time!
// This prevents accidentally passing product ID to user function
2. Named Types vs Underlying Types
Key concept: Named types are completely separate, even with same base.
type Age int
type Score int
var a Age = 25
var s Score = 25
var n int = 25
// All store integers, but:
// a = s // β ERROR - Different types!
// a = n // β ERROR - Different types!
// Must convert:
a = Age(s) // β
a = Age(n) // β
Why this exists:
Prevents mixing values with different meanings:
type Celsius float64
type Fahrenheit float64
func boilWater(temp Celsius) {
if temp >= 100 {
fmt.Println("Boiling!")
}
}
var c Celsius = 100
var f Fahrenheit = 212
boilWater(c) // β
Correct
// boilWater(f) // β ERROR - Prevents mixing units!
3. Interface to Interface Conversion
Core concept: Go checks the actual value inside the interface.
Real-Life Analogy (Helps Understanding)
Think of people and skills:
Person can drive β call them a Driver
Person can write β call them a Writer
Same person can be both only if they have both skills
The Go Example
type Reader interface {
Read()
}
type Writer interface {
Write()
}
type File struct{}
func (File) Read() {
fmt.Println("Reading...")
}
// File can Read, but cannot Write yet
Put File in Reader interface:
var r Reader = File{} // β
Works - File has Read()
Try to convert to Writer:
var w Writer = r.(Writer) // π₯ PANIC!
Why panic?
rcontains aFilevalueGo asks: "Does File have Write() method?"
Answer: NO
Result: PANIC
The key insight:
Go doesn't look at the interface type (Reader)
Go looks at the actual value inside (File)
"What's really inside?"
If it satisfies target interface β β
OK
If not β β PANIC
Making It Work
Give File the Write method:
func (File) Write() {
fmt.Println("Writing...")
}
// Now File has both Read() and Write()
Now conversion works:
var r Reader = File{}
var w Writer = r.(Writer) // β
Works!
w.Write() // "Writing..."
Safe Way (Always Use!)
var r Reader = File{}
w, ok := r.(Writer)
if ok {
fmt.Println("Can write!")
w.Write()
} else {
fmt.Println("Cannot write")
}
One-line rule:
"Interface conversion depends on the real value inside, not the interface name"
4. Assignability Rules
When can you assign one value to another?
Value x of type V is assignable to variable of type T if:
- V and T are identical
var a int = 10
var b int = a // β
- V and T have identical underlying types, and one is unnamed
type MyInt int
var a MyInt = 10
// var b int = a // β ERROR
var b int = int(a) // β
Must convert
- T is an interface and V implements it
type Stringer interface {
String() string
}
type Person struct{ Name string }
func (p Person) String() string {
return p.Name
}
var p Person = Person{Name: "Alice"}
var s Stringer = p // β
Person implements Stringer
- V is nil and T is a reference type
var p *int = nil // β
var s []int = nil // β
var m map[string]int = nil // β
5. Comparable Types
Types that can use == and != operators.
Comparable:
Boolean, integer, float, string
Pointer, channel, interface
Struct (if all fields comparable)
Array (if elements comparable)
Not comparable:
- Slice, map, function
// Comparable
var a int = 10
var b int = 10
fmt.Println(a == b) // β
true
// Not comparable
var s1 = []int{1, 2}
var s2 = []int{1, 2}
// fmt.Println(s1 == s2) // β ERROR
// Use reflect.DeepEqual for slices:
import "reflect"
fmt.Println(reflect.DeepEqual(s1, s2)) // β
true
Struct comparison:
type Point struct {
X, Y int
}
p1 := Point{1, 2}
p2 := Point{1, 2}
fmt.Println(p1 == p2) // β
true
// But not if contains non-comparable field:
type Container struct {
Data []int // Slice not comparable
}
c1 := Container{[]int{1}}
c2 := Container{[]int{1}}
// fmt.Println(c1 == c2) // β ERROR
6. Methods on Types
Value vs Pointer Receivers
type Counter struct {
count int
}
// Value receiver - gets a COPY
func (c Counter) GetCount() int {
return c.count
}
// Pointer receiver - gets ORIGINAL
func (c *Counter) Increment() {
c.count++ // Modifies original
}
c := Counter{count: 0}
c.Increment()
fmt.Println(c.GetCount()) // 1
c.Increment()
fmt.Println(c.GetCount()) // 2
When to use:
Value receiver: Just reading data
Pointer receiver: Modifying data or large structs
Method Sets
type T struct{}
func (t T) ValueMethod() {}
func (t *T) PointerMethod() {}
// Type T method set: {ValueMethod}
// Type *T method set: {ValueMethod, PointerMethod}
var t T
t.ValueMethod() // β
t.PointerMethod() // β
Go auto-converts to (&t).PointerMethod()
var pt *T = &T{}
pt.ValueMethod() // β
pt.PointerMethod() // β
7. Type Embedding (Composition)
type Animal struct {
Name string
}
func (a Animal) Speak() string {
return "Some sound"
}
type Dog struct {
Animal // Embedded - Dog "inherits" fields/methods
Breed string
}
dog := Dog{
Animal: Animal{Name: "Buddy"},
Breed: "Golden Retriever",
}
fmt.Println(dog.Name) // β
Access embedded field
fmt.Println(dog.Speak()) // β
Access embedded method
8. Pointer Type Conversion (Restricted)
Go's rule: Pointers are tied to exact types.
type Person struct {
Name string
Age int
}
type Employee struct {
Name string
Age int
}
p := Person{Name: "Alice", Age: 30}
pp := &p
// var ep *Employee = pp // β ERROR
// Even with identical fields!
Why? Memory safety and type safety.
The unsafe Bypass:
While Go forbids direct pointer conversion, you can do it using unsafe.Pointer:
import "unsafe"
p := Person{Name: "Alice", Age: 30}
pp := &p
// Standard way - ERROR
// var ep *Employee = pp // β
// Using unsafe - WORKS but DANGEROUS
ep := (*Employee)(unsafe.Pointer(pp))
fmt.Println(ep.Name) // Alice
// β οΈ This works but:
// - Not guaranteed by Go spec
// - Can break with compiler changes
// - Dangerous if structs have different layouts
// - Generally discouraged in production
When you might need unsafe:
Interfacing with C code (cgo)
Extremely performance-critical code
Low-level system programming
Interview answer: "I know about
unsafe.Pointerfor pointer conversion, but it bypasses Go's type safety and should only be used when absolutely necessary, like in cgo or low-level optimizations."
Better alternative:
e := Employee{
Name: p.Name,
Age: p.Age,
}
9. Slice Type Conversion
Cannot convert slices directly, even with same underlying type.
type MyInt int
a := []MyInt{1, 2, 3}
// b := []int(a) // β ERROR
Must convert element by element:
b := make([]int, len(a))
for i, v := range a {
b[i] = int(v)
}
For large slices - pre-allocate:
// Bad - append reallocates
var b []int
for _, v := range a {
b = append(b, int(v)) // Slow
}
// Good - allocate once
b := make([]int, len(a))
for i, v := range a {
b[i] = int(v) // Fast
}
10. The Nil Interface Trap π¨ CRITICAL Interview Question!
The most famous Go gotcha:
Q: Is an interface holding a nil pointer equal to nil?
A: NO!
Understanding Interface Internals
An interface value is a pair: (Type, Value)
var i interface{} // (nil, nil) - truly nil
fmt.Println(i == nil) // true
The trap:
var p *int = nil
var i interface{} = p // (*int, nil) - NOT truly nil!
fmt.Println(p == nil) // true
fmt.Println(i == nil) // FALSE! π±
Why?
A truly
nilinterface:(nil, nil)- no type, no valueInterface holding nil pointer:
(*int, nil)- has type, value is nilSince
(*int, nil) β (nil, nil), the check fails!
Real-World Bug Example
func getUser(id int) *User {
if id < 0 {
return nil // Return nil pointer
}
return &User{ID: id}
}
func main() {
var user interface{} = getUser(-1) // Assigns nil pointer to interface
if user == nil {
fmt.Println("No user") // You expect this...
} else {
fmt.Println("User exists") // ...but THIS prints! π
}
}
Output: User exists (WRONG!)
Why it happens:
user = (*User, nil) // Has type, so not truly nil
How to Fix It
Option 1: Check before assigning to interface
u := getUser(-1)
if u != nil {
var user interface{} = u
// ...
}
Option 2: Use concrete type
user := getUser(-1) // Keep as *User, not interface{}
if user == nil {
fmt.Println("No user") // β
Works correctly
}
Option 3: Reflection check (rarely needed)
import "reflect"
if user == nil || reflect.ValueOf(user).IsNil() {
fmt.Println("No user")
}
Interview Answer
"An interface is only nil if it has no type AND no value. An interface holding a nil pointer has a type, so it's not considered nil."
The Technical Details (Senior Level)
Go interfaces internally have two fields:
_type- pointer to type informationdata- pointer to actual value
type interface struct {
_type *rtype // Type information
data unsafe.Pointer // Actual value
}
Truly nil interface:
_type = nil
data = nil
Interface with nil pointer:
_type = *int // Has type!
data = nil // Value is nil
This is why the equality check fails!
11. Struct Tags and Conversion
Tags are metadata for struct fields (used by JSON, databases, etc.).
type User struct {
ID int `json:"id" db:"user_id"`
Name string `json:"name" db:"user_name"`
Password string `json:"-" db:"password"` // "-" = ignore
Created string `json:"created_at,omitempty"`
}
user := User{ID: 1, Name: "Alice"}
jsonData, _ := json.Marshal(user)
fmt.Println(string(jsonData))
// Output: {"id":1,"name":"Alice","created_at":""}
Common tag options:
json:"name"- field name in JSONjson:"-"- skip fieldjson:"name,omitempty"- skip if zero value
Struct Conversion Exception
Important: Struct tags are ignored when checking convertibility!
type User1 struct {
ID int `json:"id"`
Name string `json:"name"`
}
type User2 struct {
ID int `xml:"id"` // Different tag!
Name string `xml:"name"`
}
u1 := User1{ID: 1, Name: "Alice"}
u2 := User2(u1) // β
Works! Tags ignored in conversion
fmt.Printf("%+v\n", u2) // {ID:1 Name:Alice}
Rule: Two structs are convertible if they have:
Same field names (in same order)
Same field types
Tags don't matter!
But this fails:
type User3 struct {
Name string // Different field order!
ID int
}
// u3 := User3(u1) // β ERROR - field order matters
Part 5: Interview Preparation
Common Interview Questions
Q1: Why does this fail?
var a int = 10
var b float64 = 5.5
c := a + b // β ERROR
Answer: Go doesn't allow implicit type conversion. Must convert explicitly:
c := float64(a) + b // β
Q2: What's the difference?
var x interface{} = 10
y := int(x) // β COMPILE ERROR
z := x.(int) // β
Works
Answer:
int(x)is type conversion - only works with concrete typesx.(int)is type assertion - extracts type from interfaceSince
xis interface, must use type assertion
Q3: Is this safe?
var x interface{} = "hello"
s := x.(string)
Answer: Not safe! If x isn't string, program panics. Better:
if s, ok := x.(string); ok {
// Use s safely - Comma-Ok idiom
} else {
// Handle wrong type
}
Use the "comma-ok idiom" terminology in interviews!
Q4: The Nil Interface Trap π¨ MOST ASKED!
func getUser(id int) *User {
if id < 0 {
return nil
}
return &User{ID: id}
}
var user interface{} = getUser(-1)
if user == nil {
fmt.Println("No user")
} else {
fmt.Println("Has user")
}
// What prints?
Answer: "Has user" (surprising!)
Why? Interface is (*User, nil) not (nil, nil). It has a type, so not truly nil.
Rule: An interface is only nil if both type and value are nil.
Fix:
u := getUser(-1)
if u == nil { // Check before assigning to interface
fmt.Println("No user") // β
Works
}
Q5: Are these the same type?
type MyInt int
type YourInt int
var a MyInt = 10
var b YourInt = 10
Answer: No! Both are named types based on int, but they're different types. Cannot assign without conversion:
// a = b // β ERROR
a = MyInt(b) // β
Q5: Struct Conversion with Tags
type User1 struct {
ID int `json:"id"`
Name string `json:"name"`
}
type User2 struct {
ID int `xml:"id"`
Name string `xml:"name"`
}
u1 := User1{ID: 1, Name: "Alice"}
u2 := User2(u1) // Does this work?
Answer: β Yes! Tags are ignored in struct conversion.
Rule: Structs are convertible if they have same fields (name, type, order). Tags don't matter.
Q6: Are these the same type?
type Age int
var ages []Age = []int{1, 2, 3} // β
Answer: Cannot convert []int to []Age directly. Must convert elements:
intSlice := []int{1, 2, 3}
var ages []Age
for _, v := range intSlice {
ages = append(ages, Age(v))
}
Q7: What's wrong here?
var s string = "100"
n := int(s)
Answer: Compile error! Cannot convert string to int directly. Use:
n, err := strconv.Atoi(s) // β
Q8: What happens?
var b byte = 65
fmt.Println(string(b))
fmt.Println(strconv.Itoa(int(b)))
Answer:
Line 1:
"A"- converts byte 65 to character (ASCII)Line 2:
"65"- converts to string representation
string() on numbers gives character!
Q9: Explain output
var b byte = 65
fmt.Println(string(b))
fmt.Println(strconv.Itoa(int(b)))
Answer:
Line 1:
"A"- converts byte 65 to character (ASCII)Line 2:
"65"- converts to string representation
string() on numbers gives character!
Q10: What's the difference?
var s1 []int // nil slice
s2 := []int{} // empty slice
s3 := make([]int, 0) // empty slice
Answer:
s1isnil(not initialized)s2ands3are empty slices (initialized, length 0)s1 == nilistrues2 == nilands3 == nilarefalse
Q11: Will this work?
type Reader interface { Read() }
type Writer interface { Write() }
type File struct{}
func (File) Read() {}
var r Reader = File{}
var w Writer = r.(Writer)
Answer: Panics! File only has Read(), not Write(). Safe version:
if w, ok := r.(Writer); ok {
// Use w
} else {
fmt.Println("Doesn't implement Writer")
}
Q12: Float to int output?
fmt.Println(int(9.9))
fmt.Println(int(-9.9))
Answer:
9 // Truncates, doesn't round
-9 // Truncates toward zero
Float to int always truncates (removes decimal).
Quick Reference Tables
Type Conversion Cheat Sheet
| From β To | Syntax | Example | Result |
| int β float64 | float64(v) | float64(10) | 10.0 |
| float64 β int | int(v) | int(9.9) | 9 |
| string β int | strconv.Atoi(s) | strconv.Atoi("123") | 123 |
| int β string | strconv.Itoa(n) | strconv.Itoa(123) | "123" |
| byte β char | string(v) | string(65) | "A" |
| string β []byte | []byte(s) | []byte("Go") | [71 111] |
| []byte β string | string(b) | string([]byte{71,111}) | "Go" |
Type Categories
| Category | Types | Zero Value |
| Basic | int, float64, string, bool | 0, 0.0, "", false |
| Composite | array, slice, struct, map | varies |
| Reference | pointer, channel, interface, function | nil |
| Custom | named types | depends on base |
Conversion vs Assertion
| Aspect | Type Conversion | Type Assertion |
| Works with | Concrete types | Interfaces only |
| Syntax | Type(value) | value.(Type) |
| When | Compile time | Runtime |
| Can fail? | Overflow only | Yes, can panic |
| Safe form | N/A | val, ok := x.(Type) |
Best Practices
β DO
- Always use explicit conversion
var i int = 10
var f float64 = float64(i)
- Use safe type assertions
if val, ok := x.(string); ok {
// Use val
}
- Use named types for clarity
type UserID int
type ProductID int
- Use type switches for multiple types
switch v := x.(type) {
case int, string:
// ...
}
- Check errors from strconv
n, err := strconv.Atoi(s)
if err != nil {
// Handle error
}
β DON'T
- Don't assume implicit conversion
var a int = 10
var b float64 = 5.5
// c := a + b // β
- Don't use unsafe assertions
s := x.(string) // β Can panic
- Don't use string() for numbers
n := 65
s := string(n) // β Gives "A", not "65"
- Don't forget nil checks
var m map[string]int
// m["key"] = 10 // β Panic
m = make(map[string]int)
m["key"] = 10 // β
Summary: Master These Concepts
Core Principles
No implicit conversion - Always explicit
Named types are distinct - Even with same base
Type safety over convenience - Go prioritizes correctness
Interfaces hold values - Assertions extract them
Use the right tool - Conversion vs assertion
Key Syntax
Type(value)β Type conversion (concrete types)value.(Type)β Type assertion (interfaces)value, ok := x.(Type)β Safe assertionswitch x.(type)β Type switch
Remember
Use
strconvfor string β numberFloat to int truncates (doesn't round)
string(number)gives character, not representationnil slice β empty slice
Check if key exists in maps
Always handle errors
Interview-Ready Statement
"Go has a strictly-typed system with explicit conversions, ensuring type safety. It includes basic types (int, float64, string, bool), composite types (array, slice, struct, map), reference types (pointer, channel, function, interface), and supports custom named types. Type conversion uses Type(value) for concrete types, while type assertion uses value.(Type) for extracting types from interfaces. A critical gotcha is that an interface holding a nil pointer is not equal to nilβan interface is only nil if both its type and value are nil. All operations are explicit, making Go programs predictable and safe. When doing type assertions, I always use the comma-ok idiom to avoid panics."
Practice Checklist
Before your interview, make sure you can:
[ ] Explain all basic types and their zero values
[ ] Describe the difference between arrays and slices
[ ] Explain when to use pointers
[ ] Show how interfaces work with examples
[ ] Demonstrate type conversion vs type assertion
[ ] Explain named types vs aliases
[ ] Use type switches correctly
[ ] Handle string β number conversions
[ ] Explain why Go doesn't allow implicit conversion
[ ] Describe method receivers (value vs pointer)
[ ] Explain the nil interface trap π¨
[ ] Use "comma-ok idiom" terminology for type assertions
[ ] Explain struct tag conversion exception
[ ] Know when
unsafe.Pointermight be used (and why to avoid it)
Senior-Level Talking Points
When you want to impress in interviews:
Use correct terminology:
"Comma-ok idiom" for safe type assertions
"Underlying type" vs "named type"
"Method set" for interfaces
"itab" (interface table) for advanced discussions
Mention the nil interface trap:
"An interface is a pair of (type, value)"
"Only nil when both are nil"
Show awareness of unsafe:
"I know about
unsafe.Pointerfor bypassing type safety""Used in cgo or extreme optimizations"
"Generally avoided in production"
Explain why Go is strict:
"Prevents unit mixing bugs"
"Makes code explicit"
"Catches errors at compile time"
You're now fully prepared for your Go types interview! π
Review the Quick Reference Tables and the Nil Interface Trap section 30 minutes before your interview.

