Go

Go를 Go처럼 사용하기(Effective Go) 2

2020.02.02


(Control Structures, Functions)

Effective Go 문서를 읽어보면서, Golang에 대한 이해를 높이고 좋은 코드를 작성하는 것이 목표입니다.

 

Control structures

제어문의 경우 C와 비슷하지만 다른 점이 몇가지 있습니다.

  • do, while loop가 없고, for문으로 반복문을 대체합니다.
  • switch문이 더 유연하게 사용됩니다.
  • ifswitch 문에서는 for문에서 초기값 설정하듯 변수 선언을 할 수 있습니다.
  • breakcontinue문은 무엇을 break, continue할지 정의할 수 있습니다.
  • select문으로 여러 상황(condition)에 대한 대처를 할 수 있습니다.
  • 괄호를 사용하지 않습니다.

If

// (1)
if x > 0 {
  return y
}

// (2)
if err := file.Chmod(0664); err != nil {
  log.print(err)
  return err
}

// (3)
f, err := os.Open(name)
if err != nil {
  return err
}
codeUsing(f)

(1) go에서 if문을 사용하는 일반적인 형태입니다.

(2) 초기값 선언을 할 수 있습니다. 이는 Go에서 자주 사용되는 방식입니다. err는 if문 안에서의 지역변수로 사용될 수 있습니다.

(3) break, continue, goto, return 등으로 마무리 되어, else를 사용하지 않아도 될 경우, 불필요한 else는 생략합니다.

f, err := os.Open(name)
if err != nil {
  return err
}
d, err := f.Stat()
if err != nil {
  f.Close()
  return err
}
codeUsing(f, d)

위의 코드를 읽어보면, err가 존재할 경우, err를 return하고 작업이 성공적으로 진행됐을 때, fd를 사용하는 코드를 작성하게 된다는 흐름을 볼 수 있습니다. 여기서 불필요한 else문은 작성하지 않은 것도 확인할 수 있습니다.

 

Redeclaration and reassignment

위의 예시에서 := 문으로 변수 선언을 간략하게 하였습니다.

f, err := Os.Open(name)
// ...
d, err := f.Stat()
// ...

또한 err 변수가 두번 새로 선언되었다고 볼 수 있는데, 이는 괜찮은 방법입니다. 두번째 선언에서는 그저 재할당(reassignment) 되었을 뿐입니다. 즉, 두번째 err가 선언될 때는, 기존에 존재한 err 변수에 새로운 값을 할당시켜준 겁니다.

이러한 방식은 긴 if-else 문이 있을 경우 자주 사용되는 것을 볼 수 있습니다.

 

For

// C에서의 for문 역할
for init; condition; post { }

// C에서의 while 역할
for condition {}

// C에서 for(;;) - 무한루프
for {}

 

Array, slice, string, map, channel로부터 값을 읽는 등의 경우, range 절을 사용하여 반복문을 사용할 수 있습니다.

for key, value := range oldMap {
  newMap[key] = value
}

// 첫번째 값만 필요한 경우, 두번째 값은 버린다.
for key := range m {
  if key.expired() {
    delete(m, key)
  }
}

// 두번째 인자만 필요한 경우,
// blank identifier( _ )를 사용하여 첫번째 값을 버린다.
sum := 0
for _, value := range arr {
  sum += value
}

 

문자열의 경우, range가 UTF-8로 인코딩 된 것을 각각의 유니코드 부분으로 파싱해줍니다. 이상한 encoding이 되어있는 부분은 1 byte의 크기로 파싱됩니다.

for pos, char := range "한국\x80어" {
  fmt.Printf("character %#U starts at byte position %d\n",char, pos)
}

그래서 위와 같은 코드를 실행시켜보면

character U+D55C '한' starts at byte position 0
character U+AD6D '국' starts at byte position 3
character U+FFFD '�' starts at byte position 6
character U+C5B4 '어' starts at byte position 7

위와 같은 결과를 얻게 됩니다.

Go에는 Comma Operator(쉼표연산자)가 없습니다. 또한, ++, --는 statement 입니다. expression이 아닙니다.

// a 배열을 뒤집기
for i,j := 0, len(a)-1 ; i < j; i, j = i+1, j-1 {
  a[i], a[j] = a[j], a[i]
}

위의 경우, 문법상 i++, j-- 같이 사용할 수 없어, 위와 같이 사용합니다.

 

Switch

Go의 switch 는 C에서보다 좀 더 일반적으로 사용됩니다. expression은 상수, 정수일 필요가 없고, 위에서 아래 방향으로 매칭될 때까지 진행됩니다. switch expression에 아무것도 없으면 true로 판단하고 진행합니다. 그래서 if-else-if-else 같은 구조를 switch로 표현할 수 있습니다.

func unhex(c byte) byte {
  switch {
  case '0' <= c && c < '9':
    return c - '0'
  case 'a' <= c && c <= 'f':
    return c - 'a' + 10
  case 'A' <= c && c <= 'F':
   return c - 'A' + 10  
  }
  return 0
}

// case는 comma로 분리된 list형태로 주어질 수도 있습니다.
func shouldExcape(c byte) bool {
  switch c {
  case ' ', '?', '&', '=', '#', '+', '%':
   return true   
  }
  return false
}

C와 달리 go에서는 switch에서 break를 잘 사용하지 않지만, break를 사용함으로써 case를 일찍 마무리 할 수 있습니다. 또한 어떤 loop에 라벨을 부여할 수 있고, break로 라벨에 해당하는 loop를 빠져나갈 수 있습니다.

Loop:
 for n := 0; n < len(src); n += size {
  switch {
  case src[n] < sizeOne:
   if validateOnly {
    break
   }
   size = 1
   update(src[n])
  case src[n] < sizeTwo:
   if n+1 >= len(src) {
    err = errShortInput
    break Loop
   }
   if validateOnly {
    break
   }
   size = 2
   update(src[n] + src[n+1]<<shift)
  }
 }

continue문 또한 loop에 한해서 label을 지정할 수 있습니다.

 

Type switch

switch문은 interface 변수의 동적인 타입을 발견하는데 사용될 수도 있습니다.

var t interface{}
t = functionOfSomeType()
switch t := t.(type) { // 동일한 이름으로 생성하는 것이 관용적입니다.
default:
  fmt.Printf("unexpected type %T\n", t)
case bool:
  fmt.Printf("boolena %t\n", t)
case int:
  fmt.Printf("integer %d\n", t)
case *bool:
  fmt.Printf("pointer to boolean %t\n", *t)
case *int:
  fmt.Printf("pointer to integer %d\n", *t)  
}

 

Functions

Multiple return values

Go의 특징 중 하나는 함수 or 메소드가 여러개의 값을 return할 수 있다는 것입니다. C에서는 write error가 발생했을 때, negative count가 신호가 되어 error 상태를 볼 수 있습니다. Go에서는 이를 다음과 같이 처리합니다.

func (file *File) Write(b []byte) (n int, err error)

그래서, 정상적으로 작동할 경우, 몇 바이트를 썼는지가 n에 담길 것이고, n != len(b) 일 경우, err에는 non-nil 값이 들어가게 되겠지요. 이런식으로 함수를 작성하는 것이 일반적입니다.

 

Named result parameters

즉, return 될 값을 미리 초기화 시킬 수 있는 것입니다.

func calculate(a, b int) (res int, err error) {
  err = nil
  if a < 1 {
    res = a + b
    return
  }
  return
}

이상한 예 입니다만, 만약 a에 2 값이 전달된다면, if 문 내부가 실행되지 않으므로, res는 int 변수를 선언시 초기값인 0으로 return될 것입니다.

 

Defer

Go의 defer문은 어떤 함수가 종료되기 전에 실행될 것을 예약하는 것과 같습니다.

func Contents(filename string) (string, error) {
  f, err := os.Open(filename)
  if err != nil {
    return "", err
  }
  defer f.Close() // 모든 작업이 완료된 후, 실행할 것 (1)
  
  var result []byte
  buf := make([]byte, 100)
  for {
    n, err := f.Read(buf[0:])
    result = append(result, buf[0:n])
    if err != nil {
      if err == io.EOF {
        break
      }
      return "", err // 여기서 return시 f.Close() 실행
    }
  }
  return string(result), nil // 여기서 return시 f.Close() 실행
}

(1) 과 같이 defer을 사용하면 좋은 점은, 우선 close 할 것을 잊는 실수를 줄일 수 있다는 것과, open과 close를 가까이에 놓음으로써 함수의 마지막 부분에 close를 놓는 것 보다 흐름이 명확하게 잘 보인다는 것입니다.

defer는 스택의 형태로 쌓일 수 있습니다. 만약 다음과 같은 코드가 있다면,

for i := 0; i < 5; i++ {
  defer fmt.Printf("%d ", i)
}

4 3 2 1 0 의 결과를 얻게 됩니다.

다음과 같은 예를 살펴보겠습니다.

func trace(s string) string {
  fmt.Println("entering:", s) // (2), (5)
  return s
}

func un(s string) {
  fmt.Println("leaving:", s) // (7), (8)
}

func a() {
  defer un(trace("a")) // trace("a") : (5), un("a") : (7)
  fmt.Println("in a") // (6)
}

func b() {
  defer un(trace("b")) // trace("b") : (2), un("b") : (8)
  fmt.Println("in b") // (3)
  a() // (4)
}

func main() {
  b() // (1)
}

출력되는 순서는 다음과 같게 됩니다.

entering: b
in b
entering: a
in a
leaving a
leaving b

 

 

References