Post

go

go 언어 개념 & 문법 정리

go

go 기본 개념

package

모든 go 프로그램은 패키지들로 구성됩니다. go 프로그램은 main 패키지로부터 시작합니다. 컨벤션으로 패키지 명은 import 경로의 마지막 엘리멘트 명과 동일합니다. 예를들어 math/rand 패키지는 rand로 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
package main

import (
	"fmt"
	"math/rand"
)

func main() {
	fmt.Println("My favorite number is", rand.Intn(10))
}

import

다음과 같이 import 문을 작성할 수도 있지만, factoring 된 import 문을 사용하는 것이 권장됩니다.

1
2
import "fmt"
import "math"

go 에서 대문자로 시작하는 이름은 export 될 수 있습니다. 예를 들어 Pizza는 대문자로 시작하기에 export 됩니다. math에서 Pi도 대문자로 시작하기에 export 됩니다.

pizza 그리고 pi 는 대문자로 시작하지 않기에 export 되지 않습니다.

패키지를 import 하고 사용할 때 export 된 이름만 사용할 수 있습니다. export되지 않은 변수명은 패키지 외부에서는 사용할 수 없습니다.

1
2
3
4
5
6
7
8
9
10
package main

import (
	"fmt"
	"math"
)

func main() {
	fmt.Println(math.pi)
}

위 코드는 math 패키지에서 import한 변수를 사용할 때, export 되지 않은 변수명을 사용했기에 에러 메시지를 발생시킵니다. 오류를 고치기 위해서는 pi를 대문자로 수정해야 합니다.

function

go 함수는 파라미터를 전달 받지 않거나 다수의 파라미터를 받을 수 있습니다. 아래 코드에서 add 함수는 int 타입의 두 파라미터를 전달 받습니다. 변수의 타입이 변수 명 다음에 오는 것도 확인할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

func add(x int, y int) int {
	return x + y
}

func main() {
	fmt.Println(add(42, 13))
}

만약 연속적인 함수 파라미터가 동일한 타입이 타입 할당을 생략하고 마지막에 명시할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

func add(x, y int) int {
	return x + y
}

func main() {
	fmt.Println(add(42, 13))
}

함수는 하나 이상의 결과를 리턴할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
package main

import "fmt"

func swap(x, y string) (string, string) {
  return y, x
}

func main() {
  a, b := swap("hello", "world")
  fmt.Println(a, b)
}

go 에서 리턴할 값을 명명할 수 있습니다. 만약 그렇다면, 리턴 값은 함수 최상단에서 정의한 변수와 같이 사용할 수 있습니다. 이 이름들은 반환 값의 의미를 문서화하는 데 사용합니다. 인자가 없는 return 문은 이름이 지정된 반환 값들을 반환합니다. 이것을 naked return이라고 합니다. naked return 문은 가독성을 위해 짧은 함수에서만 사용해야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func split(sum int) (x, y int) {
  x = sum * 4 / 9
  y = sum - x
  return
}

func main() {
  fmt.Println(split(17))
}

variable

var을 이용해서 여러 변수를 선언할 수 있습니다. 함수 파라미터와 마찬가지로 타입은 마지막에만 명시할 수 있습니다. var은 패키지 레벨 혹은 함수 레벨 모두에서 사용가능 합니다.

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

var c, python, java bool

func main() {
  var i int
  fmt.Println(i, c, python, java)
}

var은 변수마다 생성자를 포함해서 선언할 수 있습니다. 만약 생성자를 사용했다면, 타입은 생략할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

var i, j, int = 1, 2

func main() {
  var c, python, java = true, false, "no!"
  fmt.Println(i, j, c, python, java)
}

함수 내부에서는 := 단축 할당문을 암시적 타입의 var 선언 대신 사용할 수 있습니다.

함수 외부에서는 모든 문이 var, func 등의 키워드로 시작하므로 := 구문을 사용할 수 없습니다.

1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

func main() {
  var i, j, int = 1, 2
  k := 3
  c, python, java := true, false, "no"

  fmt.Println(i, j, k, c, python, java)
}

go에서 기본 타입은 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bool

string

int int8 int16 int32 int64
uint uint8 uint16 uint32 uintptr

byte // uint8 

rune // int32

float32 float64

complex64 complex128

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
  "fmt"
  "math/cmplx"
)

var (
  ToBe bool = false
  MaxInt uint64 = 1<<64 - 1
  z complex128 = cmplx.Sqrt(-5 + 12i)
)

func main() {
  fmt.Printf("Type : %T Value : %v\n", ToBe, ToBe)
  fmt.Printf("Type : %T Value : %v\n", MaxInt, MaxInt)
  fmt.Printf("Type : %T Value : %v\n", z, z)
}

변수가 타입 할당된 채로 선언되고 초기 값을 가지지 않는다면, zero 값을 가지게 됩니다. 타입 별로 zero 값은 다음과 같습니다.

  • 숫자 타입일 경우 : 0
  • boolean 타입일 경우 : false
  • 문자열 타입일 경우 : “”
1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

func main() {
  var i int
  var f float64
  var b bool
  var s string
  fmt.Printf("%v %v %v %q\n", i, f, b, s)
}

T(v) 를 사용해서 값 v를 타입 T로 변환할 수 있습니다.

다음처럼 명시적 타입 할당을 사용해서 변환 가능합니다.

1
2
3
var i int = 42
var f float64 = float64(i)
var u uint = uint(f)

혹은 다음과 같이 간단하게도 사용할 수 있습니다.

1
2
3
i := 42
f := float64(i)
u := uint(f)

C와는 다르게 타입을 변환할 때, 명시적 선언이 필요합니다.

명시적으로 타입을 선언하지 않고 변수를 선언할 때 변수의 타입은 할당하는 값에서 추출해서 할당될 수 있습니다.

만약 변수에 할당되는 것의 타입이 정해져있다면, 새로운 변수도 같은 타입을 가집니다.

1
2
var i int
j := i // j 도 int 타입으로 할당됩니다.

오른쪽에 선언되는 값의 타입이 명시되지 않은 숫자라면, 할당된 숫자에 맞게 타입이 할당됩니다.

1
2
3
i := 42 // int
f := 3.142 // float64
g := 0.867 + 0.5i // complex128

const 키워드를 사용해서 상수를 선언할 수 있습니다. constant는 := 키워드를 이용해서는 선언될 수 없습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

const Pi = 3.14

func main() {
	const World = "世界"
	fmt.Println("Hello", World)
	fmt.Println("Happy", Pi, "Day")

	const Truth = true
	fmt.Println("Go rules?", Truth)
}

숫자 상수는 고정밀 값을 가집니다. 타입이 지정되지 않은 상수은 사용되는 문맥에 따라 필요한 타입을 가집니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import "fmt"

const (
  // 1을 왼쪽으로 100개 쉬프트했기에, 100자리수로 생성됩니다.
  Big = 1 << 100
  // 1을 왼쪽으로 100번 쉬프트, 그 다음 오른쪽으로 99번 옮겼기에 왼쪽으로 한번 쉬프트한 값, 2입니다.
  Small = Big >> 99
)

func needInt(x int) int {return x*10 + 1}
func needFloat(x float64) float64 {
  return x * 0.1
}

func main() {
  fmt.Println(needInt(Small))
  fmt.Println(needFloat(Small))
  fmt.Println(needFloat(Big))
}

// 21
// 0.2
// 1.2676506002282295e+29

needInt(Big) 을 실행해보면, 너무 큰 값이기에 int로 할당할 수 없다는 오류 메시지를 확인할 수 있습니다.

for

go에서는 단 한가지의 반복문, for loop만 존재합니다.

for loop는 3가지 컴포넌트로 구성됩니다.

  • 첫번째 반복 이전에 실행되는 컴포넌트
  • 매 반복 이전에 확인되는 조건문
  • 매 반복 이후에 실행되는 선언문

다음과 같이 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

func main() {
  sum := 0
  for i := 0; i < 10; i++ {
    sum += i
  }
  fmt.Println(sum)
}

init, 그리고 post 선언문은 다음과 같이 생략 가능합니다.

1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

func main() {
  sum := 1
  for ; sum < 100; {
    sum += sum
  }
  fmt.Println(sum)
}

init, post를 생략하면 while문 처럼 세미 콜론도 생략해서 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

func main() {
  sum := 1
  for sum < 100 {
    sum += sum
  }
  fmt.Println(sum)
}

조건문을 생략하면 무한 루프를 사용할 수 있습니다.

1
2
3
for {

}

if

if 조건문도 for반복문처럼 ()을 생략해서 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
  "fmt"
  "math"
)

func sqrt(x float64) string {
  if x < 0 {
    return sqrt(-x) + "i"
  }
  return fmt.Sprint(math.Sqrt(x))
}

func main() {
  fmt.Println(sqrt(2), sqrt(-4))
}

for문처럼, 조건 확인 전에 짧은 선언문을 작성할 수 있습니다. 선언문 내부에서 선언된 변수는 if문 내부에서만 사용 가능합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
  "fmt"
  "math"
)

func pow (x, n, lim float64) float64 {
  if v := math.Pow(x, n); v < lim {
    return v
  }
  return lim
}

func main() {
  fmt.Println(
    pow(3, 2, 10),
    pow(3, 3, 20)
  )
}

if의 짧은 선언문 안에서 선언된 변수는 어느 else블럭에서도 사용가능합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
  "fmt"
  "math"
)

func pow(x, n, float64) float64 {
  if v := math.Pow(x, n); v < lim {
    return v
  } else {
    fmt.Printf("%g >= %g \n", v, lim)
  }
  return lim
}

func main() {
  fmt.Println(
    pow(3, 2, 10),
    pow(3, 3, 20)
  )
}

switch

switch 선언문을 사용해서 if-else 선언문을 짧게 사용할 수 있습니다. go 에서는 break 문이 각 case에 자동으로 포함되어 있어서 다음처럼 사용 가능합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
  "fmt"
  "runtime"
)

func main() {
  fmt.Print("Go runs on")
  switch os := runtime.GOOS; os {
    case "darwin":
      fmt.Println("OS X")
    case "linux":
      fmt.Println("Linux")
    default:
      fmt.Printf("%s\n", os)

  }
}

switch 문은 top -> bottom으로 동작하기에 i == 0 인 경우 f() 는 호출되지 않습니다.

1
2
3
4
switch i {
  case 0:
  case f():
}

defer

defer 선언문은 함수의 리턴 시점까지 특정 함수의 실행을 늦춥니다. 감싸는 함수가 리턴될 때, defer선언문으로 넘긴 함수가 실행됩니다.

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

func main() {
  defer fmt.Println("world")

  fmt.Println("hello")

}

여러 defer 선언문을 적용하면, stack 방식으로 동작합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func main() {
	fmt.Println("counting")

	for i := 0; i < 10; i++ {
		defer fmt.Println(i)
	}

	fmt.Println("done")
}

pointers

go에서는 포인터가 있습니다. 포인터는 메모리 주소 값을 저장하는 변수입니다. *T 타입은 T 값에 대한 포인터를 의미하고, zero 값은 nil입니다. &연산자를 이용해 포인터 변수를 생성할 수 있습니다.

1
2
3
var p *int
i := 42
p = &i

*연산자를 이용해 포인터 주소에 있는 값을 사용할 수 있습니다.

1
2
fmt.Println(*p) // i 값을 포인터 p를 이용해서 읽어옵니다.
*p = 21 // i 값을 포인터 p를 이용해 세팅합니다.

이를 dereferencing 혹은 indirecting이라고 부릅니다.

struct

struct는 필드의 모음입니다.

1
2
3
4
5
6
7
8
9
10
11
12
package main

import "fmt"

type Vertex struct {
  X int
  Y int
}

func main() {
  fmt.Println(Vertex{1, 2})
}

struct의 필드는 . 를 이용해 접근 가능합니다.

1
2
3
v := Vertex{1, 2}
v.X = 4
fmt.Println(v.X) // 4

struct의 필드는 포인터를 통해 접근할 수 있습니다. struct 포인터 p를 통해 필드 X에 접근하려면 (*p).X로 작성해야하지만, p.x로 작성해도 사용가능합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

type Vertex struct {
  X int
  Y int
}

func main() {
  v := Vertex{1, 2}
  p := &v
  p.X = 1e9
  fmt.Println(v)
}

struct 변수를 생성할 때, Name: 으로 필드의 일부만 값을 저장할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import "fmt"

type Vertex struct {
  X, Y int
}

var (
  v1 = Vertex{1, 2}
  v2 = Vertex{X : 1} // Y 값은 zero value로 할당됩니다.
  v3 = Vertex{} // X, Y 값 모두 zero value로 할당됩니다.
)

func main() {
  fmt.Println(v1, p, v2, v3)
}

arrays

[n]Tn개의 T 타입 어레이를 생성할 수 있습니다.

1
2
3
var a[10]int

var primes[6]int{2, 3, 5, 7, 11, 13}

slices

배열은 고정된 크기를 가졌지만, slice는 동적으로 크기를 조절할 수 있습니다. []T 명령어로 T 슬라이스를 생성가능합니다.

1
2
primes := [6]int{2, 3, 5, 7, 11, 13}
s := primes[1:4] // slice로 타입이 할당됩니다.

슬라이스는 어레이의 레퍼런스 입니다. 슬라이스는 데이터를 저장하지 않고, 참조하는 배열의 특정 섹션을 나타냅니다. 슬라이스의 값을 변경하는 것은 원본 배열의 값을 변경하게 됩니다. 특정 섹션을 공유하는 다른 슬라이스의 값도 변경됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import "fmt"

func main() {
  names := [4]string {
    "John",
    "Paul",
    "George",
    "Ringo"
  }
  fmt.Println(names)

  a := names[0:2]
  b := names[1:3]
  fmt.Println(a, b)

  b[0] = "XXX"
  fmt.Println(a, b)
  fmt.Println(names)
}

// [John Paul George Ringo]
// [John Paul] [Paul George]
// [John XXX] [XXX George]
// [John XXX George Ringo]

다음과 같이 슬라이스를 생성하면, 먼저 배열이 생성되고, 해당 배열을 참조하는 슬라이스가 생성됩니다.

1
[]bool{true, true, false}

아래 배열에 대해서 다음 슬라이스 표현들은 모두 동일하게 적용됩니다.

1
2
3
4
5
6
var a [10]int

a[0:10]
a[:10]
a[0:]
a[:]

slice는 length와 capacity라는 값을 가집니다.

  • length : 슬라이스가 현재 담고 있는 값의 개수
  • capaicty : 원본 배열이 저장할 수 있는 값의 개수 length, capacity 값은 len(s) 그리고 cap(s)로 확인 가능합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
s := []int{2, 3, 5, 7, 11, 13}
printSlice(s)

s = s[:0]
printSlice(s)

s = s[:4]
printSlice(s)

s = s[2:]
printSlice(s)

func printSlice(s []int) {
  fmt.Printf("len= %d cap = %d %v\n", len(s), cap(s), s)
}

// len=6 cap=6 [2 3 5 7 11 13]
// len=0 cap=6 []
// len=4 cap=6 [2 3 5 7]
// len=2 cap=4 [5 7]

slice의 zero value는 nil 입니다. nil slice는 length, capacity 값이 0 이고 원본 배열이 존재하지 않습니다.

slice는 make함수로 생성해서 동적인 크기를 가지는 배열을 생성할 수 있습니다.

1
2
3
4
a := make([]int, 5) // len(a) == 5

b := make([]int, 0, 5) // capacity를 5로 설정
b = b[:cap(b)] // len(b) == 5 cap(b) == 5

slice에 값 추가는 append함수를 이용해서 추가할 수 있습니다.

1
func append(s []T, vs ...T) []T

range

range 를 이용해서 slice 혹은 map를 순회할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}

func main() {
  for i, v := range pow {
    fmt.Printf("2**%d = %d\n", i, v)
  }
}

for i, _ := range pow // index 만 필요할때
for i := range pow // index만 필요할때
for _, v := range pow // value만 필요할 때

map

map 은 키와 밸류 값을 쌍으로 저장할 수 있습니다. map의 zero value는 nil 입니다. nil map은 키를 가지지 안호 키를 추가할 수도 없습니다. make함수를 이용해 초기화를 한 후에 사용가능합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

type Vertex struct {
  Lat, Long float64
}

var m map[string]Vertex

func main() {
  m = make(map[string]vertex)
  m["Bell labs"] = Vertex{
    40.68433, -74.39967
  }
  fmt.Println(m["Bell labs"])

}

다음과 같이 선언할 수도 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

type Vertex struct {
  Lat, Long float64
}

var m= map[string]Vertex {
  "Bell labs": Vertex{
    40.68433, -74.39967
  }, 
  "Goole": Vertex{
    37.42202, -122.08408
  }
}

func main() {
  fmt.Println(m)
}

다음과 같이 타입을 생략하는 것도 가능합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main 

import "fmt"

type Vertex struct {
  Lat, Long float64
}

var m = map[string]Vertex {
  "Bell labs": {40.68433, -74.39967}
  "Google" : {37.42202, -122.08408}  
}

func main() {
  fmt.Println(m)
}

맵에 값 추가, 값 조회, 값 삭제 등등은 다음과 같이 할 수 있습니다.

1
2
3
4
5
m[key] = elem // 값 추가
elem = m[key] // 값 조회
delete(m, key) // 값 삭제
elem, ok = m[key] // ok가 true면 값 존재, false면 없음,
elem, ok := m[key] // elem, ok가 선언되지 않았으면 이렇게 사용할 수 있습니다.

function values

함수도 값처럼 다뤄질 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
  "fmt"
  "math"
)

func compute(fn func(float64, float64) float64) float64 {
  return fn(3, 4)
}

func main() {
  hypot := func(x, y float64) float64 {
    return math.Sqrt(x*x + y*y)
  }

  fmt.Println(hypot(5, 12))

  fmt.Println(compute(hypot))
  fmt.Println(compute(math.Pow))

}

function closure

go 함수는 클로저일 수 있습니다. 클로저는 함수 본문 외부의 변수를 참조하는 함수 값입니다. 함수는 참조된 변수에 접근하고 할당할 수 있으며, 이런 의미에서 함수는 변수에 묶여 있다고 할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package main

import "fmt"

func adder() func(int) int {
  sum := 0
  return func(x int) int {
    sum += x
    return sum

  }

}

func main() {
  pos, neg = adder(), adder()
  for i := 0; i < 10; i++ {
    fmt.Println(
      pos(i),
      neg(-2*i)
    )
  }

}

---

package main

import "fmt"

// fibonacci is a function that returns
// a function that returns an int.
func fibonacci() func() int {
	fibo := make([]int,1)

	return func() int {
		if len(fibo) == 1 {
			fibo = append(fibo, 1)
			return 0
		} else {
			fibo = append(fibo, fibo[len(fibo)-1] + fibo[len(fibo)-2])
			return fibo[len(fibo)-1]
		}
	}
}

func main() {
	f := fibonacci()
	for i := 0; i < 10; i++ {
		fmt.Println(f())
	}
}

method

go에서는 클래스가 존재하지 않습니다. 대신 type에 메소드를 정의할 수 있습니다. 메소드는 특별한 리시버 argument를 포함한 함수입니다. 다음과 같이 리시버 함수를 정의할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import "fmt"
import "math"

type Vertex struct {
  X, Y float64
}

func (v Vertex) Abs() float64 {
  return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
  v := Vertex{3, 4}
  fmt.Println(v.Abs())
}

non-struct 타입에도 메소드를 선언할 수 있습니다. 하지만 같은 패키지에 속한 타입에만 메소드를 선언할 수 있습니다. float64같은 타입에 메소드를 추가하기 위해서는 다음과 같이 MyFloat로 감싸서 선언해야합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
  "fmt"
  "math"
)

type MyFloat float64

func (f MyFloat) Abs() float64 {
  if f < 0 {
    return float64(-f)
  }
  return float64(f)
}

func main() {
  f := MyFloat(-math.Sqrt2)
  fmt.Println(f.Abs())
}

pointer receivers

pointer를 리시버로 가지는 메소드를 선언할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
  "fmt"
  "math"
)

type Vertex struct {
  X, Y float64
}

func (v Vertex) Abs() float64 {
  return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func (v *Vertex) Scale(f float64) {
  v.X = v.X * f
  v.Y = v.Y * f
}

func main() {
  v := Vertex{3, 4}
  v.Scale(10)
  fmt.Println(v.Abs())  
}

Scale 메소드는 포인터 리시버 함수 입니다. 포인터 리시버 함수는 포인터가 가리키는 값을 변경할 수 있습니다. 메소드는 종종 리시버의 값을 변경하는 용도로 쓰이기에, 포인터 리시버는 값 리시버보다 흔하게 쓰이곤합니다.

값 리시버 메소드로 Scale 함수를 작성하면, Scale메소드로는 원본 Vertex의 copy가 전달되고, 원본 데이터의 값을 변경할 수 없게 됩니다.

포인터 리시버로 선언한 메소드는 (&v).Scale()처럼 사용하지 않고 v.Scale()로도 사용 가능하다는 장점이 있습니다. 그 반대도 가능합니다. 값 타입 리시버로 선언한 메소드를 포인터 변수에도 사용 가능합니다.

1
2
3
4
var v Vertex
fmt.Println(v.Abs()) // ok
p := &v
fmt.Println(p.Abs()) // ok

포인터 리시버는 두가지 이유로 사용합니다.

  • 메소드를 이용해 리시버의 값을 변경하기 위해서
  • 메소드 호출마다 값의 복사를 막기 위해서

보통의 경우 어떤 타입의 메소드는 값 리시버 혹은 포인터 리시버 중 하나만을 가지며 혼용하는 것은 권장되지 않습니다.

interface

인터페이스 타입은 메소드 시그니처들을 정의합니다. 인터페이스 타입의 값으로는 해당 인터페이스를 구현하는 어떤 것이든 될 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main

import (
  "fmt"
  "math"
)

type Abser interface {
  Abs() float64
}

type MyFloat float64

type Vertex struct {
  X, Y float64
}

func (f MyFloat) Abs() float64 {
  if f < 0 {
    return float64(-f)
  }
  return float64(f)
}

func (v *Vertex) Abs() float64 {
  return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
  var a Abser
  f := MyFloat(-math.Sqrt2)
  v := Vertex{3,4}

  a = f // MyFloat는 Abser를 구현해야합니다.
  a = &v // *Vertex는 Abser를 구현해야 합니다.

  // a = v, Vertex는 Abser를 구현하지 않았기에 할당 불가합니다.

  fmt.Println(a.Abs())
}

인터페이스에 정의된 메소드를 구현할 때, 추가적인 명시 없이 인터페이스에 정의된 메소드를 정의하는 것으로 인터페이스를 구현할 수 있습니다.

내부적으로 인터페이스 값은 값과 구체적인 타입의 튜플로 생각할 수 있습니다:

(value, type), 인터페이스 값은 특정한 구체적인 타입의 값을 담고 있습니다.

인터페이스 값에서 메서드를 호출하면, 해당 메서드는 그 값의 구체적인 타입에서 동일한 이름의 메서드를 실행합니다.

만약 인터페이스의 value가 nil이라면, 메소드는 nil 리시버로 실행됩니다. 몇몇 언어에서는 null pointer exception이 발생하지만, go에서는 이를 핸들링할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package main

import (
  "fmt"

)

type I interface {
	M()
}

type T struct {
	S string
}

func (t *T) M() {
	if t == nil {
		fmt.Println("<nil>")
		return
	}
	fmt.Println(t.S)
}

func main() {
	var i I

	var t *T
	i = t
	describe(i)
	i.M()

	i = &T{"hello"}
	describe(i)
	i.M()
}

func describe(i I) {
	fmt.Printf("(%v, %T)\n", i, i)
}

만약 nil 인터페이스 value일때, 해당 값의 타입도 알 수 없다면, 런타임 에러가 발생하게 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

type I interface {
	M()
}

func main() {
	var i I
	describe(i) 
	i.M() // i의 타입이 없기에 에러 발생
}

func describe(i I) {
	fmt.Printf("(%v, %T)\n", i, i)
}

empty interface

아무런 타입도 명시하지 않은 인터페이스를 empty interface라고 합니다. empty interface는 어떤 타입이라도 받을 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

func main() {
  var i interface {}
  describe(i)

  i = 42
  describe(i)

  i = "hello"
  describe(i)

}

func describe(i interface{}) {
  fmt.Printf("(%v %T)\n", i, i)
}

// (<nil>, <nil>)
// (42, int)
// (hello, string)

type assertions

type assertion은 인터페이스 값의 구체적인 값을 접근할 수 있도록 해줍니다.

1
t := i.(T)

위 코드는 인터페이스 값 i가 구체적인 타입 T를 가지고 있으며, 그 구체적인 T 값을 변수t에 할당하는 코드입니다.

만약 iT타입이 아니라면, 에러가 발생합니다. 특정 타입을 가지고 있는지 확인하기 위해 다음과 같이 사용할 수도 있습니다.

1
t, ok := i.(T)

type switch

다음과 같은 type switch 문을 이용해서 다양한 type assertion을 가능하게 할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func do(i interface{}) {
  switch v := i.(type) {
    case int:
      fmt.Printf("Twice %v is %v\n", v, v*2)
    case string:
      fmt.Printf("%q is %v bytes long\n", v, len(v))
    default:
      fmt.Printf("unknown %T\n", v)
  }
}

func main() {
  do(21)
  do("hello")
  do(true)
}

Stringers

Stringerfmt패키지에 정의된 인터페이스로 toString()과 같은 역할을 한다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

import "fmt"

type Person struct {
	Name string
	Age  int
}

func (p Person) String() string {
	return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}

func main() {
	a := Person{"Arthur Dent", 42}
	z := Person{"Zaphod Beeblebrox", 9001}
	fmt.Println(a, z)
}

---
package main

import "fmt"

type IPAddr [4]byte

// TODO: Add a "String() string" method to IPAddr.

func (v IPAddr) String() string {
	return fmt.Sprintf("%v.%v.%v.%v", v[0], v[1], v[2], v[3])
}

func main() {
	hosts := map[string]IPAddr{
		"loopback":  {127, 0, 0, 1},
		"googleDNS": {8, 8, 8, 8},
	}
	for name, ip := range hosts {
		fmt.Printf("%v: %v\n", name, ip)
	}


Errors

go 프로그램은 error 값들로 에러 상태를 표현합니다. error 타입은 fmt.Stringer와 유사한 빌트인 인터페이스 입니다.

1
2
3
type error interface {
  Error() string
}

함수들은 종종 error값을 리턴하고 error값이 nil인 경우 함수가 성공했다는 의미입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
	"fmt"
	"time"
)

type MyError struct {
	When time.Time
	What string
}

func (e *MyError) Error() string {
	return fmt.Sprintf("at %v, %s",
		e.When, e.What)
}

func run() error {
	return &MyError{
		time.Now(),
		"it didn't work",
	}
}

func main() {
	if err := run(); err != nil {
		fmt.Println(err)
	}
}

Readers

io 패키지는 데이터 스트림을 읽는 io.Reader인터페이스를 정의하고, 인터페이스에 Read() 메소드를 정의했습니다.

Reader는 다음과 같이 사용 가능합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
	"fmt"
	"io"
	"strings"
)

func main() {
	r := strings.NewReader("Hello, Reader!")

	b := make([]byte, 8)
	for {
		n, err := r.Read(b)
		fmt.Printf("n = %v err = %v b = %v\n", n, err, b)
		fmt.Printf("b[:n] = %q\n", b[:n])
		if err == io.EOF {
			break
		}
	}
}

This post is licensed under CC BY 4.0 by the author.