Skip to main content

Command Palette

Search for a command to run...

Types In Go - Complete Guide By Aman Gupta.

Updated
β€’29 min read
Types In Go - Complete Guide By Aman Gupta.
A

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 int container holds whole numbers

  • A string container holds text

  • You can't put text in an int container!


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:

TypeZero Value
int, int8, int16...0
float32, float640.0
boolfalse
string"" (empty)
pointernil
slicenil
mapnil
channelnil
interfacenil
functionnil

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

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?

  1. 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!)
  1. 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 type

  • JSON 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?

  1. 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
  1. Add methods:
type Age int

func (a Age) IsAdult() bool {
    return a >= 18
}

myAge := Age(20)
fmt.Println(myAge.IsAdult())  // true
  1. 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 for uint8 (0-255) - single byte

  • rune = alias for int32 - 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!

AspectType ConversionType Assertion
Works withConcrete typesInterfaces only
SyntaxType(value)value.(Type)
WhenCompile timeRuntime
Can fail?Overflow onlyYes, 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:

  • val gets actual value with correct type

  • No assertions needed inside cases

  • type keyword 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?

  1. r contains a File value

  2. Go asks: "Does File have Write() method?"

  3. Answer: NO

  4. 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:

  1. V and T are identical
var a int = 10
var b int = a  // βœ…
  1. 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
  1. 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
  1. 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.Pointer for 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 nil interface: (nil, nil) - no type, no value

  • Interface holding nil pointer: (*int, nil) - has type, value is nil

  • Since (*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 information

  • data - 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 JSON

  • json:"-" - skip field

  • json:"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:

  1. Same field names (in same order)

  2. Same field types

  3. 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 types

  • x.(int) is type assertion - extracts type from interface

  • Since x is 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:

  • s1 is nil (not initialized)

  • s2 and s3 are empty slices (initialized, length 0)

  • s1 == nil is true

  • s2 == nil and s3 == nil are false


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 β†’ ToSyntaxExampleResult
int β†’ float64float64(v)float64(10)10.0
float64 β†’ intint(v)int(9.9)9
string β†’ intstrconv.Atoi(s)strconv.Atoi("123")123
int β†’ stringstrconv.Itoa(n)strconv.Itoa(123)"123"
byte β†’ charstring(v)string(65)"A"
string β†’ []byte[]byte(s)[]byte("Go")[71 111]
[]byte β†’ stringstring(b)string([]byte{71,111})"Go"

Type Categories

CategoryTypesZero Value
Basicint, float64, string, bool0, 0.0, "", false
Compositearray, slice, struct, mapvaries
Referencepointer, channel, interface, functionnil
Customnamed typesdepends on base

Conversion vs Assertion

AspectType ConversionType Assertion
Works withConcrete typesInterfaces only
SyntaxType(value)value.(Type)
WhenCompile timeRuntime
Can fail?Overflow onlyYes, can panic
Safe formN/Aval, ok := x.(Type)

Best Practices

βœ… DO

  1. Always use explicit conversion
var i int = 10
var f float64 = float64(i)
  1. Use safe type assertions
if val, ok := x.(string); ok {
    // Use val
}
  1. Use named types for clarity
type UserID int
type ProductID int
  1. Use type switches for multiple types
switch v := x.(type) {
case int, string:
    // ...
}
  1. Check errors from strconv
n, err := strconv.Atoi(s)
if err != nil {
    // Handle error
}

❌ DON'T

  1. Don't assume implicit conversion
var a int = 10
var b float64 = 5.5
// c := a + b  // ❌
  1. Don't use unsafe assertions
s := x.(string)  // ❌ Can panic
  1. Don't use string() for numbers
n := 65
s := string(n)  // ❌ Gives "A", not "65"
  1. 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

  1. No implicit conversion - Always explicit

  2. Named types are distinct - Even with same base

  3. Type safety over convenience - Go prioritizes correctness

  4. Interfaces hold values - Assertions extract them

  5. 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 assertion

  • switch x.(type) β†’ Type switch

Remember

  • Use strconv for string ↔ number

  • Float to int truncates (doesn't round)

  • string(number) gives character, not representation

  • nil 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.Pointer might be used (and why to avoid it)


Senior-Level Talking Points

When you want to impress in interviews:

  1. 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

  2. Mention the nil interface trap:

    • "An interface is a pair of (type, value)"

    • "Only nil when both are nil"

  3. Show awareness of unsafe:

    • "I know about unsafe.Pointer for bypassing type safety"

    • "Used in cgo or extreme optimizations"

    • "Generally avoided in production"

  4. 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.