Go의 기본 문법과 Receiver

 

 

1. 들어가며 - GO의 기본 문법

 

Go는 구글에서 만든 언어로 python, Java에 비하면 비교적 젊은 언어입니다. C++의 불편함을 개선하기 위해 처음 만들어졌고 GoRoutine 같은 비동기 매커니즘을 언어 자체에서 제공하고 있습니다. 처음 Go를 접하신 분이라면 다음 글에서 빠르게 문법을 훑고 지나가는 것을 권해드립니다! 30분 G의 제목처럼 keyword가 25개 밖에 되지 않아 입문이 어렵지 않습니다. 

 

https://school.programmers.co.kr/learn/courses/13/13-30%EB%B6%84-go

 

30분 Go

현재 IOS/안드로이드 앱 내에서는 결제를 지원하지 않습니다.

school.programmers.co.kr

 

 

처음 Go에 대해 작성하는 글이고 처음 접하는 분들도 많을 것이라 생각하여 간략하게 여기에도 요약하겠습니다. :)

 

1.1 변수 선언

 

var <변수명> <type> = <초기값>

 

Kotlin에서 콜론이 빠진 듯한 형태를 가지고 있습니다.

 

1.2 Type conversion

 

Go에는 묵시적 형변환이 존재하지 않습니다. 명시적으로 

 

type(변수명)

 

와 같이 해줘야 합니다.

 

1.3 포인터의 존재

 

c++을 대체하려고 나온만큼 포인터가 있습니다..!

 

1.4 반복문과 조건문

 

둘 모두 특이하게 boolean 식에 소괄호 ()를 사용하지 않습니다. 중괄호 {} 는 사용해야 합니다.

그리고 go에는 while문이 존재하지 않습니다.

 

1.5 collection

 

Go에도 배열이 있습니다. var (변수명) [(크기)](자료형) 와 같은 방식으로 선언됩니다.또한 slice라는 독특한 존재가 있는데 c++의 vector와 유사합니다. 배열과 달리 고정된 크기를 미리 지정하지 않을 수 있고, 그 크기를 동적으로 변경하거나 부분 배열을 추출할 수도 있습니다.

 

아래의 코드 예제를 보면 더 직관적으로 이해가 갈 것 같습니다 :)

// 1.1 변수 선언
var natural int = 1
var complex float = 2
// :=
// func 바깥에서 사용 불가능.
// 그 외 https://stackoverflow.com/questions/17891226/difference-between-and-operators-in-go 참조
a := 2

// 1.2 형변환
var i int = 100
var u uint = uint(i)

// 1.4 조건문과 반복문

if a == 1{
	fmt.Println("I'm one")
}

for {
 // 무한 루프
}

// 1.5 Collection

// array
var a [3]int

// slice
var s []int

// map
var goMap map[int]string
goMap = make(map[int]string)

 

 

그 외에 closure, variadic function 등 추가 개념이 있지만 모든 개념을 정리하는 것은 무리가 있고 오늘은 receiver만 다뤄보도록 하겠습니다!

 

2. Go의 Receiver

 

Go에는 클래스(class)가 없습니다. 대신 구조체(struct)가 있고 이를 위한 메소드를 따로 정의할 수 있습니다. 이러한 메소드는 Receiver 인자가 있는 특별한 함수입니다.

 

예제로 먼저 보는 것이 빠를 것 같습니다.

 

X, Y를 가지는 Point 구조체를 선언하고 해당 구조체를 위한 메소드를 선언하는 방법을 살펴볼까요?

 

type Point struct {
    X, Y float64
}


// value receiver
func (v Point) absReceiver() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}


func (p Point) mul(a int) {
   p.X *= a
   p.Y *= a
}

// not receiver
func absNotReceiver(v Point) float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

// point receiver

func (p *Point) add(a int)  {
   p.X += a
   p.Y += a
}

func main() {
    v := Point{3, 4}
    
    // receiver
    fmt.Println(v.absReceiver())
    
    // not receiver
    fmt.Println(absNotReceiver(v))
    
    // point receiver 작동 원리   
    v.add(10)
    v.mul(100)
    fmt.Println(v)
}

 

위처럼 func 키워드와 메소드 이름 사이의 목록에 있는 (v Point)가 receiver 인자입니다! 아래의 receiver 인자가 아닌 함수 파라미터로 받는 경우와 비교하면 이해하기 쉽습니다. 

 

재밌는 점은 위처럼 구조체 형식에도 메소드를 선언할 수 있다는 점입니다 :)

 

receiver에는 value receiver와 pointer receiver 두 가지가 있습니다. value receiver는 call by value를 pointer receiver는 call by reference를 따라가고 있습니다. 즉 pointer를 떠올려본다면 그 차이를 알 수 있습니다.

 

위 코드에서 pointer receiver의 결과는 어떻게 될까요? 바로 13, 14입니다. 위에서 add는 pointer receiver이고 mul은 value receiver 입니다. 그렇기 때문에 mul은 값이 복제되어서 들어가고 add는 레퍼런스가 넘어가서 원본 값 자체를 바꿀 수 있습니다. 즉 값을 안에서 바꾸고 싶다면 pointer receiver를 아니라면 value receiver를 사용하면 됩니다!

 

그럼 언제 pointer receiver를 사용하면 좋을까요? 상황 별로 다르겠지만 간단하게 요약하면 다음과 같습니다.

- receiver의 값을 변경하고자 할 때(단순히 읽기가 아닌 쓰기 작업도 같이) 
- struct의 크기가 커서 deep copy 비용이 클 때
- 코드 일관성(option): 어떤 함수가 포인터 receiver를 사용하는 경우 일관성을 줄 때

 

포인터를 써보신 분이라면 위 코드에서 이상한 점을 느낄 것 같습니다.

 

v.add(10) 에서 우리는 v를 포인터로 선언한 적이 없는데 잘 실행이 되고 있습니다. 원래라면 (&v).add(10) 이어야 할 것 같은데 말이죠! 이는 Go에서 편의상 포인터 리시버일 경우 값이든 포인터든 receiver 인자로는   (&v).add(10)처럼 해석해주기 때문입니다. 이는 함수 인자에는 해당되지 않습니다.

 

 

참고 자료

 

https://stackoverflow.com/questions/17891226/difference-between-and-operators-in-go

https://hyanggeun.github.io/posts/golang-pointer/

https://velog.io/@chltpdus48/Go-%ED%8F%AC%EC%9D%B8%ED%84%B0-%EB%A6%AC%EC%8B%9C%EB%B2%84%EC%99%80-%EA%B0%92-%EB%A6%AC%EC%8B%9C%EB%B2%84

https://velog.io/@whdnjsdyd111/GO-2-5.-%EA%B3%A0%EB%9E%AD-%EA%B8%B0%EB%B3%B8%EB%AC%B8%EB%B2%95-%EB%A6%AC%EC%8B%9C%EB%B2%84-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4