선언문이 있는 각각의 공간을 block
이라고 부른다. 함수 외부에서 선언된 변수, 상수, 타입, 함수는 package
내에 속한다. package
내에 함수의 맨 상위에 있는 모든 선언문들, 변수들은 file
블록 내에 있게 된다.
함수 내에서 중괄호({}
) 세트는 다른 블록을 정의하며 go의 제어 구조가 자체 블록을 정의한다는 것을 볼 수 있다.
어떤 내부 블록에서 어떤 외부 블록에 선언된 식별자를 접근할 수 있다. 그렇다면 포함된 블록 사이에서 같은 이름을 가지는 식별자를 선언했다면 무슨 일이 발생할 지 의문이 생길 것이다. 만약, 이렇게된다면 외부 블록에 생성된 식별자를 내부에서 쉐도잉(shadowing
)한다고 한다.
다음의 코드를 보도록 하자.
func main() {
x := 10
if x > 5 {
fmt.Println(x)
x := 5
fmt.Println(x)
}
fmt.Println(x)
}
결과는 다음과 같다.
10
5
10
쉐도잉 변수는 포함된 블록 내에 이름이 같은 변수가 있는 것을 의미한다. 쉐도우 변수가 존재하는 한 쉐도잉 대상이 된 변수는 접근할 수 없다.
즉, if
문 내에 main
함수 블록에 있는 x
변수와 이름이 같은 x
라는 변수를 새롭게 선언함으로써 쉐도잉을 하게 된 것이다. 이에 따라 if
문의 두 번째 fmt.Println
문은 5가 출력된 것이다. if
문이 종료됨에 따라 if
문의 블럭이 종료되고 선언된 변수인 x
가 해제된다. 단, main
함수 블럭에 선언된 변수 x
는 남아있기 떄문에 이를 출력하는 것이다.
이런 일이 발생하는 이유는 :=
연산자를 사용하는 것이 정확히 어떤 변수를 대상으로 일어나는 지 불명확하기 때문에 발생한 일이다. :=
를 사용할 떄, 우연히 쉐도잉된 변수일 수 있기 때문이다. :=
연산자를 이용해 여러 개의 변수를 한번에 생성하고 값을 할당할 수 있다는 것을 기억하자.
또한, :=
왼쪽에 모든 변수들이 새롭게 선언된 변수가 아니어도 사용이 가능하다. 즉, 왼쪽에 적어도 한 개의 변수만 새롭게 선언되더라도 :=
을 사용 가능하다.
다음의 예시를 보도록 하자.
func main() {
x := 10
if x > 5 {
x, y := 5, 10
fmt.Println(x, y) // 5 10
}
fmt.Println(x) // 10
}
if
문 외부 블럭에 이미 x
변수가 선언되었다. 그리고 if
문에 같은 이름인 x
변수를 :=
를 사용했으니, 쉐도잉 변수 x
를 갖게된다. 별 문제 없어보이지만, 만약, if
문안에 main
에 있는 외부 변수 x
의 값을 설정해주는 로직을 넣었다면 이는 문제가 있다. 왜냐하면 쉐도잉 된 if
문 안의 x
변수에 의해 if
문 밖의 x
에 접근할 수가 없기 때문이다.
사실 원래의 의도는 if
문 안에 y
라는 변수를 사용하기위해 :=
을 사용하려고 했다. 그러나 :=
은 새로운 변수를 선언하고 값을 할당하는 초기화를 하기 떄문에 해당 if
문 블럭에는 x
가 없어 같은 이름이지만 if
문에만 유효한 새로운 변수 x
를 새로 만들고 값을 대입한 것이다. 이런 실수를 별로 안할 것 같지만 다음의 코드를 보자.
func processing() (int, error) {
return 2, nil
}
func main() {
var x int
if x == 0 {
x, err := processing()
if err != nil {
fmt.Println(x, err)
}
}
fmt.Println(x)
}
위의 코드는 변수 x
가 zero value일 때 processing
함수를 사용하여 값을 채우는 것이다. processing
함수는 int, error
를 반환하기 때문에 error
를 받는 변수를 따로 만들어주어야 한다. 그래서 :=
를 사용했더니 x
가 쉐도잉이 되어버린다. 즉 if
안의 같은 이름이지만 새로운 변수 x
가 생겨버린 것이다. 이 값에는 2가 들어가지만 외부의 x
는 가려져서 값이 채워지지 않는다.
0
따라서 다음과 같이 0이라는 결과를 얻게 된다. 이처럼 err
를 처리하기위해 무의식적으로 :=
을 사용하면 이전 변수를 쉐도잉하는 문제를 겪을 수 있다. 해결방법은 매우 간단하다. err
변수를 따로 선언하거나 processing
의 반환값을 임시 변수에 받아서 외부의 x
에 할당해주면 된다.
func processing() (int, error) {
return 2, nil
}
func main() {
var x int
if x == 0 {
temp, err := processing()
if err != nil {
fmt.Println(x, err)
}
x = temp
}
fmt.Println(x)
}
결과는 5
가 나올 것이다.
쉐도잉을 조심해야하는 것은 패키지 블럭에서도 마찬가지이다. 만약 패키지 이름을 쉐도잉하면 원치 않은 결과를 볼 수 있을 것이다.
func main() {
x := 10
fmt.Println(x)
fmt := "oops"
fmt.Println(fmt) // error
}
fmt
패키지를 쉐도잉해버리면 해당 블럭에서 fmt
패키지에 접근할 수가 없게된다.
만약, 이미 만든 프로그램에서 쉐도잉이 무섭다면 shadow
툴이 있으니 다운받아서 사용하길 바란다.
go의 내장 타입, 상수, 함수, nil은 유니버스 블록이라는 곳에 미리 선언된 식별자(predeclared identifier)에 불과하다. 이 유니버스 블록은 모든 파일 블록에서 접근이 가능하다. 때문에 유니버스 블록에 정의된 어떤 식별자든 재정의되지 않도록 매우 조심해야한다. 이는 쉐도잉을 해버리기 때문에 예기치 못한 에러를 발생시킬 수 있다.
go에서의 if
문이 다른 언어와 가장 큰 차이점을 보이는 것은 감싸는 괄호가 없다는 것이다.
if condition {
}
그런데 go에서는 if
문에서 다른 언어들과 달리 재밌는 재밌는 점이 있는데, 일반적으로 대부분의 언어는 if
,else
문의 중괄호 내에 선언된 모든 변수는 블록 내에서만 유효하다. go에서 추가된 것은 조건 if
혹은 else
블록 내에서만 사용가능한 변수를 선언할 수 있다는 것이다. 이런 특성을 이용하여 다음의 코딩이 가능하다.
func main() {
if n := rand.Intn(10); n == 0 {
fmt.Println("That is too low")
} else if n > 5 {
fmt.Println("That's too big", n)
} else {
fmt.Println("That's a good number", n)
}
}
즉 if
문의 조건이 들어가는 condition
에 변수를 선언할 수 있다는 것이다. 그리고 이런 변수는 if
, else if
, else
블록 내에서 모두 접근이 가능하다.
이런 특별한 범위 변수를 가지는 것은 매우 편리한데, 변수를 생성하는데 해당 변수가 필요한 영역에서만 사용 가능하도록 되기 때문이다. 일단 if/else
문들이 마무리되면 n
은 더이상 접근되지 않는다. 즉, if-else
블록에 사용되기를 위해 특별한 변수를 외부에서 선언한 다음 끌고 올 필요가 없다는 것이다.
go의 유일한 반복문인 for
문이다. go는 네가지 다른 방법으로 for
키워드를 사용할 수 있다.
for
문for
(while방식)for
for-range
c언어 방식의 for
문은 다음과 같다.
for i := 0; i < 10; i++ {
fmt.Println(i)
}
0~9까지의 숫자를 출력한다. 사용법은 c
언어와 같고 특별한 부분은 딱히 없다. 몇 가지 세부사항이 있는데
:=
를 사용하도록 하자. 즉 var
키워드는 허용하지않는다. if
문의 변수 선언과 마찬가지로 변수 쉐도잉이 될 수 있다. 만약 for
문 외부의 다른 변수를 반복자로 쓰고 싶다면 :=
을 쓰지 않도록 하자.두 번째 부분은 조건식이므로 boolean
만 나오면 된다.
세번째 부분은 증감이다.
while
과 같은 방식이다.
for i < 100 {
}
for
문에 조건식도 사용하지 않는 방법이다.
for {
fmt.Println("hello")
}
참고로 go는 do-while
문을 제공하지 않는다. 최소한 1번의 실행을 보장받아야 한다면 for
에서 if
문을 사용하도록 하자.
무한 루프를 벗어나거나, 반복의 상황에서 벗어나기 위해 사용하는 키워드로 break
와 continue
이다.
네 번째 for-range
문은 go
의 내장 타입의 요소를 순회하며 루프를 수행하는 for
문이다. 다른 언어에서 iterator
을 사용하여 반복하는 것과 비슷하다.
문자열, 배열, 슬라이스, 맵, 채널을 가지고 for-range
루프를 사용할 수 있다. 단, for-range
는 go의 내장 타입 복합 타입이나 내장 복합 타입에 기반한 사용자 정의 타입로만 순회가 가능하다.
package main
import (
"fmt"
)
func main() {
evenVals := []int{1, 12, 3, 41}
for i, v := range evenVals {
fmt.Println(i, v)
}
}
결과는 다음과 같다.
0 1
1 12
2 3
3 41
for-range
루프는 두 개의 변수를 얻는다는 부분이 재미있다. 첫번째 변수는 현재 순회 중인 자료구조에 있는 값의 위치이고, 두 번째는 해당 위치의 값이다. 그러나, 두 루프 변수의 관용적인 이름은 루프되는 항목에 따라 다르다. 가령 배열, 슬라이스, 문자열은 순회할 때 인덱스로 첫번째 인자가 사용오고, 맵은 key가 온다. 두번째 값이 온다.
두 개의 변수를 받기 때문에 루프에서 인덱스의 변수의 접근이 필요하지 않다면 밑줄(_)를 사용하여 변수를 생략할 수 있다.
func main() {
value := []int{8, 2, 1, 4, 5, 6}
for _, v := range value {
fmt.Println(v)
}
}
위와 같이 첫번째 인자인 인덱스를 사용하고 싶지않다면 _
으로 생략하면 된다.
만약, 인덱스(혹은 키)만 사용하고 대응되는 값은 필요없다면 변수를 사용하지 않아도 된다.
func main() {
value := []int{8, 2, 1, 4, 5, 6}
for i := range value {
fmt.Println(i)
}
}
for-range
루프로 map
자료구조를 순회하는 방법에서 재미난 것이 있다.
func main() {
m := map[string]int{
"a": 1,
"b": 3,
"c": 2,
}
for i := 0; i < 3; i++ {
fmt.Println("Loop", i)
for k, v := range m {
fmt.Println(k, v)
}
}
}
이 프로그램을 실행하면 매번 출력이 다르게 나온다.
Loop 0
a 1
b 3
c 2
Loop 1
a 1
b 3
c 2
Loop 2
a 1
b 3
c 2
키와 대응되는 값의 순서가 가끔 같을 수도 있지만, 다양하게 출력될 것이다. 이것은 실제 보안 기능이다. 이전 go
버전에서는 맵에 같은 항목을 넣은 경우 맵의 키 순회 순서가 일반적으로 같았다. 이는 두 가지 문제를 야기한다.
1. 사람들은 순서가 고정된 것으로 가정하고 코드를 작성하는데, 이는 이상한 시점에 문제를 발생시킬 것이다.
2. 만약 맵이 항상 해시 값을 정확히 동일한 값을 만들고 서버에 맵으로 사용자 데이터를 저장하고 있는 경우라면, 모든 키가 동일한 버킷에 해시되어 특수 제작된 데이터를 보내는 해시 도스(Hash Dos) 공격으로 서버 속도를 느려지게 할 수 있다.
언급된 이 두 문제를 막기 위해 go는 맵 구현에 두 가지를 변경했다. 첫 번째는 맵을 위해 해시 알고리즘을 수정하여 맵 변수가 생성될 때 마다 무작위의 숫자를 포함하도록 했다. 두 번째는 맵을 for-range
로 순회의 순서를 루프가 반복될 때 마다 조금씩 달라지게 했다. 이 두가지의 변경으로 해시 도스 공격을 더 어렵게 한다.
이 규칙에는 단 하나의 예외가 있다. 맵을 디버깅과 로깅을쉽게 하기 위해 포메팅 함수(fmt.Println과 같은 함수)는 항상 오름차순으로 맵의 키를 출력한다.
문자열에도 for-range
루프를 적용할 수 있다.
package main
import (
"fmt"
)
func main() {
samples := []string{"hello", "apple!"}
for _, sample := range samples {
for i, r := range sample {
fmt.Println(i, r, string(r))
}
}
fmt.Println()
}
다음의 결과는 아래와 같다.
0 104 h
1 101 e
2 108 l
3 108 l
4 111 o
0 97 a
1 112 p
2 112 p
3 9829 ♥
6 108 l
7 101 e
8 33 !
첫번째 열은 인덱스를 두 번째 열은 문자의 숫자값, 세번째는 문자의 숫자값을 문자열로 변환한 값이다.
중요한 것은 for-range
로 순회하는 것은 rune
을 순회하는 것이지 byte
를 순회하는 것이 아니다. 이는 for-range
로 문자열에 여러 바이트에 걸친 룬을 처리할 때 UTF-8
표현을 단일 32비트 숫자(rune
자체가 int32
이기 때문)로 변환하고 값에 할당한다. for-range
루프에서 만약 UTF-8
값이 아닌 것을 처리할 때는 유니코드 대체 문자 16진수 oxffd로 반환된다. 따라서 이모티콘처럼 1바이트를 초과해서 사용하는 문자도 문제없이 출력이 된다.
단, 인덱스 부분은 문자의 바이트 시작 부분이다.
3 9829 ♥
6 108 l
하트 이모티콘은 3바이트를 사용하므로 인덱스 부분은 3이다. 단, 값은 룬으로 출력되기 때문에 하트 그대로가 나오는 것이다.
정리하면, for-range
루프로 문자열의 룬을 순서대로 접근할 수 있다. 단, 인덱스로 반환되는 값은 문자 시작 부분의 바이트 수이다.
for-range
루프를 순회할 때 반복자로 사용되는 인덱스와 값은 복사한 값이다. 어떤 타입이든 포인터가 아닌 이상 가져온 값의 변수를 수정하더라도 복합 타입에 있던 값이 변경되진 않는다.
package main
import "fmt"
type People struct {
name string
age int
}
func main() {
temp1 := People{
name: "hello",
age: 12,
}
temp2 := People{
name: "world",
age: 20,
}
peopleTeam := []People{temp1, temp2}
for _, v := range peopleTeam {
v.name = "gyu"
v.age = 99
}
fmt.Println(peopleTeam) // [{hello 12} {world 20}]
}
물론 만약 포인터라면 값을 바꿀 것이다. 위의 경우는 포인터가 아닌 값이기 때문에 반복자에 복사될 뿐 원본에 영향을 주진 않는 것이다.
중첩된 for
문이 너무 깊을 때 빠져나오고 싶으면 label을 이용하여 종료하거나 건너뛰기를 할 수 있다. 만약 continue
를 사용하면 이전의 루프 정보를 반복자들에 남기고, break
를 사용하면 진짜 루프를 빠져나오기 때문에 반복문의 밖으로 나가게 된다.
func main() {
samples := []string{"hello", "apple!"}
outer:
for _, sample := range samples {
for i, r := range sample {
fmt.Println(i, r, string(r))
if r == 'l' {
continue outer
}
}
fmt.Println()
}
}
다음의 코드를 실행하면 아래의 결과를 얻을 수 있다.
0 104 h
1 101 e
2 108 l
0 97 a
1 112 p
2 112 p
3 108 l
그러나 레이블이 있는 중첩 for
문은 매우 드물기도하고 가독성이 그렇게 좋진않아 자주 쓰이진 않는다.
go도 switch-case문을 제공한다.
package main
import "fmt"
func main() {
words := []string{"a", "cow", "smile", "gopher", "octopus", "anthropologist"}
for _, word := range words {
switch size := len(word); size {
case 1, 2, 3, 4:
fmt.Println(word, "is a short word!")
case 5:
fmt.Println(word, "is exactly the right length:", size)
case 6, 7, 8, 9:
default:
fmt.Println(word, "is a long word!")
}
}
}
해당 코드를 실행하면 다음과 같은 결과를 볼 수 있다.
a is a short word!
cow is a short word!
smile is exactly the right length: 5
anthropologist is a long word!
if
문과 마찬가지로 switch
문에서 비교가 되는 값을 감쌀 필요가 없다. 재밌는 것은 switch
문의 블럭 외부에 있는 for
문의 변수인 word
에도 접근이 가능하다는 것이다.
모든 case
문은 중괄호 내에 들어가 있지만 case
문이 구성되는 내용에는 중괄호를 넣지 않는다.
c언어와 달리 switch-case
문은 모든 case
문 마지막에 break
를 넣을 필요가 없다. go는 기본적으로 case
가 하나 실행되면 해당 case
만 실행된다. 따라서, case 6, 7, 8, 9:
의 경우는 아무것도 실행되지 않는다.
물론 go에서는 하나의 case
다음 case
를 계속해서 수행할 수 있도록 fallthrough
키워드를 가지고 있다. 그러나 이 키워드를 사용하는 것부터가 이미 switch-case
문의 조건들이 잘못되었다는 것이다.
switch-cae
문에는 ==
로 비교 가능한 조건들이면 모두 가능하다.
일반 switch
문과 같이 공백 switch
문에 일부러써 간단한 변수 선을 선택적으로 포함할 수 있다. 하지만 일반 switch
문과 다르게 case
문에 로직 테스트를 넣을 수 있다.
a := 2
switch {
case a == 2:
fmt.Println("a is 2")
case a == 3:
fmt.Println("a is 3")
case a == 4:
fmt.Println("a is 4")
default:
fmt.Println("a is ", a)
}
해당 코드는 표현식 switch
를 사용하면 더 좋다.
switch a {
case 2:
fmt.Println("a is 2")
case 3:
fmt.Println("a is 3")
case 4:
fmt.Println("a is 4")
default:
fmt.Println("a is ", a)
}
사실 if
문이든 switch
문이든 너무 많이 사용하면 좋을게 없다. 코드 가독성을 떨어뜨리고 코드를 더욱 복잡하게 만든다. 최대한 의존성을 주입하여 다른 configuration 상황을 유연하게 대처할 수 있도록 하는 것이 좋다. 만약 이런 것이 어렵다면 switch-case
문은 case
간 조건이 되도록 서로 관련이 있는 것으로 되어있는게 좋다. 가령 변수 a
의 크기에 대한 조건이 필요하다면 switch-case
가 좋다. 그런데, 그런것이 아닌 여러 변수들이 여러 단계를 거쳐야하는 조건이라면 if-else if-else
가 훨씬 더 좋다.
go의 4번째 제어문으로 goto
문이 있다. go에서는 goto
문은 코드의 레이블이 지정된 줄을 명시하고 실행이 해당 라인으로 이동하도록한다. 하지만 어디든 이동할 수 있는 것은 아니다. go
는 변수 선언을 건너 뛰거나 내부 혹은 병렬 블록으로 바로 이동하는 것은 금지한다.
package main
import "fmt"
func main() {
a := 10
goto skip
b := 20
skip:
c := 30
fmt.Println(a, b, c)
if c > a {
goto inner
}
if a < b {
inner:
fmt.Println("a is less than b")
}
}
해당 프로그램을 실행하면 다음과 같은 오류를 볼 수 있다.
./main.go:7:7: goto skip jumps over declaration of b at ./main.go:8:4
./main.go:13:8: goto inner jumps into block starting at ./main.go:15:11
대부분은 사용하지 말자. 레이블 지정된 break
, continue
문을 사용하여 중첩된 루프에서 벗어나거나 순회를 건너뛸 수 있다. 아래는 goto
를 사용하는 유일한 경우를 보여준다.
func main() {
a := rand.Intn(10)
for a < 100 {
if a%5 == 0 {
goto done
}
a = a*2 + 1
}
fmt.Println("Do someething")
done:
fmt.Println("don complicated")
fmt.Println(a)
}
위와 같이 반복에서 벗어나기위해 goto
를 사용할 수 있지만 flag
를 사용하여 루프에서 벗어나는 방법이 더 좋다.