The range
keyword works the same way we use it in speech.
Example 1
Friend Asks:
What are your feelings on all of the Star Wars movies?
You say:
Eh, my feelings range from hot garbage to best movies of all time.
Example 2
If you come to my town we have a huge range of restaurants ๐ฎ๐๐บ๐ฒ ๐ฅ to choose from.
Example 3
Move in closer! You’re not in range of the camera! ๐ธ
We use range
to specify a start and an end and to move through each one of
those items until we’ve seen the whole collection. These collection of items
can be any of the ones we talked about in previous lessons (arrays, slices,
strings, maps) and a new type
channel or
chan
.
Setup
Let’s make our directory ranges
and the files we want inside of
that directory example_test.go
ranges.go
mkdir ranges
touch ranges/example_test.go ranges/ranges.go
Now let’s open up ranges.go
and for the very first line we’ll add
package ranges
Next for example_test.go
for the very first line we’ll add
package ranges_test
We can import basics/ranges
into cmd/main.go
and run functions
from there with go run cmd/main.go
and also to run our example_test.go
๐
we use go test ranges/example_test.go
in the commandline.
Understanding Why
Before we look into range
let’s understand why it’s even in the language. If
you’re new to programming you may have figured out you can grab each value in
an array by using a for loop.
myArr := [5]int{0, 1, 2, 3, 4}
for i := 0; i < 5; i++ {
num := myArr[i]
fmt.Println(num)
}
and if you recall in the
slice
lesson, when we
learned about append
we found how to get the length of a slice len(mySlice)
so you can do the same for slices.
mySlice := []int{0, 1, 2, 3, 4}
mySlice = append(mySlice, 5, 6, 7, 8)
for i := 0; i < len(mySlice); i++ {
num := mySlice[i]
fmt.Println(num)
}
So what’s the big deal? ๐ค Do we need another word we have to learn range
to
do the exact thing, we can already accomplish? Good question! ๐ Ask
everything! You’ll get very far in programming with that mentality. ๐
There are major advantages for using the range
keyword:
Lexical Scoping
Like the lesson on
if/else
shows
we can scope what we care about in the if
block. The range
keyword does the
same for our index/key and value.
Extended
It is nice to work on arrays and slices, but it also works on string
, map
and chan
types as well.
Universal
You know exactly what you’re trying to accomplish with range
unlike a for
loop
sl2 := []int{1, 2, 3}
sl1 := []string{"one", "two", "three"}
for i := 0; i < len(sl1); i++ {
s := sl2[i]
fmt.Println("my string, right? ๐", s)
}
//////////////////////////////////////////
i := 4
sl1 := []string{"one", "two", "three"}
for i < len(sl1) {
fmt.Println("Can't see me ๐๏ธ", sl1[i])
}
By Index
Now we want to know that we will start at the beginning of our slice and we want to go through all of the indexes (or indices) to the end of it, how can we know for sure that will happen?
Coding Time!
ranges.go
// Index shows us how to grab the index of each element in a slice.
func Index() {
uselessSlice := make([]struct{}, 10)
for i := range uselessSlice {
fmt.Println("index:", i)
}
// You can also inline this!
// for i := range make([]struct{}, 10) {
// fmt.Println("index:", i)
// }
}
example_test.go
func ExampleIndex() {
ranges.Index()
// Output:
// index: 0
// index: 1
// index: 2
// index: 3
// index: 4
// index: 5
// index: 6
// index: 7
// index: 8
// index: 9
}
By Value
Now we really don’t care about the index, we just want the values inside of the
slice, how can we accomplish this? In Go, you can’t have an unused variable,
but ๐ you can have an _
underscored variable. With this we’re clearly
stating to Go
I know this variable is not used and I am choosing not to use it.
Coding Time!
ranges.go
// Values shows that we can ignore the index using a range loop if we
// don't need it.
func Values() {
friends := []string{"Gabby", "Gorm", "Gunter"}
// You cannot have unused variables in Go, but you can use _ to ignore them.
for _, f := range friends {
fmt.Println("friend:", f)
}
}
example_test.go
func ExampleValues() {
ranges.Values()
// Output:
// friend: Gabby
// friend: Gorm
// friend: Gunter
}
By Index And Value
For completeness, what if we are going to use both the index and the value in a
range
statement? Then we Just Do It!
Coding Time!
ranges.go
// IndexAndValues shows how to grab both the index and the value of each
// element in a slice.
func IndexAndValues() {
nums := []int{1, 2, 3, 4, 5}
for i, n := range nums {
fmt.Printf("index: %d, access value: %d, range value: %d\n", i, nums[i], n)
// This can also be written as: nums[i] = n * n
nums[i] *= n
}
fmt.Println("nums:", nums)
}
example_test.go
func ExampleIndexAndValues() {
ranges.IndexAndValues()
// Output:
// index: 0, access value: 1, range value: 1
// index: 1, access value: 2, range value: 2
// index: 2, access value: 3, range value: 3
// index: 3, access value: 4, range value: 4
// index: 4, access value: 5, range value: 5
// nums: [1 4 9 16 25]
}
Range Through Map
We are now at a place where a simple for
loop wouldn’t help. How would we
tell the for loop to go through every key-value pair of a map with a simple
numberโ Instead of that range
makes it much easier for us and if you
remember the lesson on
maps
they and slices
share a lot of similarities. It’s just using the right tool ๐ง for the right
job ๐ฉ, they both have a key to access a value. That’s why we can use a map
just like a slice
of some type.
โ ๏ธ WARNING! โ ๏ธ
The key-value pairs that come from range
do not have to be in the order
they were placed. Meaning if you run our example_test.go
a couple of times
with go test
you’ll see it will fail! ๐คฏ This is expected! The order of a map
is not guaranteed.
Coding Time!
ranges.go
// Map shows that we can loop through the entries of a map (key and value)
// using range. Maps are not ordered in Go!
func Map() {
isMarried := map[string]bool{"Gaph": true, "Gene": false, "Gable": false}
for key, val := range isMarried {
if val == true {
fmt.Println(key, "is married.")
} else {
fmt.Println(key, "is not married.")
}
}
}
example_test.go
func ExampleMap() {
// XXX(jay): This may fail from time to time!
// There is no order in maps!
// Why not run it a few times to see? ๐
ranges.Map()
// Output:
// Gaph is married.
// Gene is not married.
// Gable is not married.
}
Range Through String
We’re going to talk in-depth about
runes
, but it’s good to know
that by using range
, a string
will produce an index and a rune
not a
byte
. This is for native support with
UTF-8
strings, or more
broadly I could say strings that support all the languages. So you can
write Go in whatever language you so please. ๐ฅฐ
Coding Time!
ranges.go
// String shows that we can get the index and rune value of each character
// in a string.
func String() {
for i, r := range "gophergo.dev" {
fmt.Println("index:", i, "rune:", r, "representation:", string(r))
}
}
example_test.go
func ExampleString() {
ranges.String()
// Output:
// index: 0 rune: 103 representation: g
// index: 1 rune: 111 representation: o
// index: 2 rune: 112 representation: p
// index: 3 rune: 104 representation: h
// index: 4 rune: 101 representation: e
// index: 5 rune: 114 representation: r
// index: 6 rune: 103 representation: g
// index: 7 rune: 111 representation: o
// index: 8 rune: 46 representation: .
// index: 9 rune: 100 representation: d
// index: 10 rune: 101 representation: e
// index: 11 rune: 118 representation: v
}
Range Through Channel
We will talk about
channels
later on after covering more
fundamental topics, but we’re talking about range
right now and if we ever
come back to this lesson we’ll have a complete look at what types range
works
with.
For now write โ๏ธ ๐๏ธ this down and come back to it after learning about channels to soak it all in ๐งฝ
Coding Time!
ranges.go
// Channel shows that we can grab values from a channel until it is
// closed.
func Channel() {
ch := make(chan string, 6)
ch <- "We can get"
ch <- "values from a channel"
ch <- "continuously."
ch <- "Just make sure"
ch <- "you close the channel"
ch <- "at some time ๐"
// NOTE(jay): Make sure the channel is closed!
// You will be stuck in an infinite loop without it.
close(ch)
for val := range ch {
fmt.Println(val)
}
}
example_test.go
func ExampleChannel() {
ranges.Channel()
// Output:
// We can get
// values from a channel
// continuously.
// Just make sure
// you close the channel
// at some time ๐
}
Lexical Scoping
We’ve talked about scoping before and we’ll talk about it some more. Scoping is
what allows certain variables to live ๐ถ and die ๐ in certain spaces. Scoping
is soooo important in programming that JS developers got so fed up with their
var
keyword
that they made two
more
,
just for scope.
In Go we don’t have those troubles, the language is very well designed, but what we do have is human minds ๐ง and without necessary information we may feel like things don’t make sense ๐ต
When I first started programming it was so weird to me that even though I
have my value from the for
loop, it wouldn’t allow me to change that
value in my collection. This is better explained with code, so…
Coding Time!
ranges.go
// ScopedValues shows that you get copies of the values of all of the
// collections (slice, string, map) that we can range over and changing those
// values does NOT change the collection.
func ScopedValues() {
scopedSlice := []int{0, 1, 2, 3, 4}
scopedString := "NOT changed"
scopedMap := map[string]bool{"x": true, "y": true, "z": true}
fmt.Println("Try to change by just the value")
for _, num := range scopedSlice {
b := num
num = 9
fmt.Println("before:", b, "after:", num)
fmt.Println("Never changes:", scopedSlice)
}
fmt.Println("Same!", scopedSlice)
for _, r := range scopedString {
b := r
r = 'X'
fmt.Println("before:", b, "after:", r)
}
fmt.Println("Same!", scopedString)
for _, val := range scopedMap {
b := val
val = false
fmt.Println("before:", b, "after:", val)
}
fmt.Println("Same!", scopedMap)
fmt.Println()
fmt.Println("Change by index (by dereference)")
for i, n := range scopedSlice {
if n < 4 {
scopedSlice[i] = 9
}
}
fmt.Println("Changed!", scopedSlice)
// NOTE(jay): Remember strings are immutable! You can't do this!
// for i, _ := range scopedString {
// scopedString[i] = byte('X')
// scopedString[i] = 'X'
// }
byteSlice := []byte(scopedString)
for i, b := range byteSlice {
// if our byte is lowercase we change it.
if b >= 'a' && b <= 'z' {
byteSlice[i] = 'X'
}
}
fmt.Println("Changed!", string(byteSlice))
for key := range scopedMap {
scopedMap[key] = false
}
fmt.Println("Changed!", scopedMap)
}
example_test.go
func ExampleScopedValues() {
ranges.ScopedValues()
// Output:
// Try to change by just the value
// before: 0 after: 9
// Never changes: [0 1 2 3 4]
// before: 1 after: 9
// Never changes: [0 1 2 3 4]
// before: 2 after: 9
// Never changes: [0 1 2 3 4]
// before: 3 after: 9
// Never changes: [0 1 2 3 4]
// before: 4 after: 9
// Never changes: [0 1 2 3 4]
// Same! [0 1 2 3 4]
// before: 78 after: 88
// before: 79 after: 88
// before: 84 after: 88
// before: 32 after: 88
// before: 99 after: 88
// before: 104 after: 88
// before: 97 after: 88
// before: 110 after: 88
// before: 103 after: 88
// before: 101 after: 88
// before: 100 after: 88
// Same! NOT changed
// before: true after: false
// before: true after: false
// before: true after: false
// Same! map[x:true y:true z:true]
//
// Change by index (by dereference)
// Changed! [9 9 9 9 4]
// Changed! NOT XXXXXXX
// Changed! map[x:false y:false z:false]
}
Scoping Wrap up
When we create a for
loop and we assign values to that for loop
for i, n := range numbers { ... }
here ๐ ๐ we can see we’re assigning values, right? Meaning we’re picking ๐ค
an index and a value from our numbers slice and assigning it to i
and n
and just like in an if/else
statement, those values are scoped ๐ญ to that
for
loop. They are copies from the numbers slice, not the actual values.