Аргументы отложенного вызова вычисляются в момент вызова defer, а не при выходе из функции. Поэтому в defer fmt.Println(i) будет зафиксировано текущее значение i на момент выполнения строки с defer. Если потом i изменится, вывод всё равно будет старым. Чтобы передать актуальное значение на момент выхода, используют замыкание: defer func() { fmt.Println(i) }().
for i := 0; i < 3; i++ {
defer fmt.Println(i) // выведет 2, 1, 0 (значения в момент регистрации)
}
defer func() {
fmt.Println(i) // выведет актуальное i при выходе
}()