Go - 기초 문법 남은 모든 것들..(Pointers 포인터, Arrays & Slices 배열, Maps 맵, Structs 구조체)

Corner·2022년 5월 17일
0

Go

목록 보기
8/9
post-thumbnail

Switch

switch문은 생소하지 않을 수 있습니다. 몇몇 언어를 제외하고 다른 언어들을 공부했다면 switch문은 모두 사용해봤을 것입니다.

switch는 기본적으로 값을 체크해주는 방법인데

예를 들어 age를 체크한다면

case 10 일 때 return ...

case 9 일 때 return ...이런 식이죠.

func switchAge(age int) bool {
   switch age {
   case 17:
      return false
   case 18:
      return true
   case 19:
      return true
   case 20:
      return true
   }
   return false
}

이렇게 사용할 수 있습니다. 마찬가지로 변수가 사용되는 곳에는 age에 괄호가 생략 되어있는거 말고는 똑같습니다.

여기서 변형해서 사용하는 방법도 있습니다.

ifelse if를 남발하지 않고 사용하는 방법입니다.

switch {
	case age < 18:
		return false
	case age == 18:
		return true
	case age > 50:
		return false
	}
	return false

만약 if문으로 써야했다면 어땠을까요?

if age < 18 {
   return false
} else if age == 18 {
   return true
} else if age > 50 {
   return false
}
return false

이런 느낌이었을 겁니다. 개인적으로 한 눈에 들어오는 것은 switch문이고, 간결해보입니다. 반면, if문은 열이 잘 맞지 않는 점과 괄호를 계속 쳐서 한 번에 알아보긴 힘드네요.

switch문에는 if문 처럼 변수 선언도 가능합니다.

switch korAge := age + 2; korAge {
case 10:
   return false
case 18:
   return true
}
return false

알아두면 나중에 쓰일수도 있는 좋은 방식인 것 같습니다.


Pointers 포인터

본인은 고등학생 시절 C언어를 독학으로 서적을 구매하여 배웠을 때 포인터라는 개념이 참 어려웠습니다.

그렇지만 포인터라는 개념을 잡아야 메모리라는 것에 대해 알게되고, 프로그래밍의 기본을 알게됩니다.

포인터라는 개념은 객체지향 프로그래밍에서는 사실 보기는 어렵습니다. 제가 알고있는 수준은 Java나 Javascript 정도(?)뿐이네요.

포인터는 메모리로 접근해서 메모리의 주소를 볼 수 있고, 메모리 주소에 저장된 값도 볼수 있습니다.

평소 코딩하는 방식대로 작성해보자면

func main() {
  a := 2
  b := a
  fmt.Println(a, b)
}

이 코드를 출력하면 당연하게도 2, 2가 나옵니다.

여기서

func main() {
   a := 2
   b := a
   a = 10
   fmt.Println(a, b)
}

이 코드를 출력하게 되면, 10, 2가 나옵니다.

메모리 주소를 보는 법과 무엇인가를 메모리에 접근하도록 하는 방법에 대해 알아보겠습니다.

처음엔 a라는 변수를 선언했고, b라는 변수는 a의 값을 가지도록 선언했습니다.

값이 복사된 것인데 b가 선언된 뒤에는 a의 값이 변해도 b 변수에는 아무런 영향이 없습니다.

프로그래밍은 위에서부터 아래로 컴파일하기 떄문입니다.

메모리 주소를 보는 법은

fmt.Println(&a, &b)

변수 앞에 &를 붙이면됩니다.

저 같은 경우엔

0x1400012c008 0x1400012c010

이 값을 출력했습니다. 이것은 이 변수가 가지고있는 메모리 주소입니다.

이 값은 환경마다 모두 다르기 때문에 이 값과 일치하지 않을 것입니다.

b변수는 a의 값을 가지게 했는데 메모리 주소가 다르다는 것도 알 수 있습니다.

값은 같아도 메모리 주소가 다릅니다.

계속 컴파일을 돌릴 때마다 메모리 주소는 변한다는 것도 알 수 있습니다.

그렇다면 b라는 변수에 a의 메모리 주소값을 가지게 하면 어떻게 될까요?

a := 2
b := &a
fmt.Println(&a, b)
0x1400001a0a8 0x1400001a0a8

같은 값으로 복제되어 출력되지만, 또 이 b의 메모리 주소값은 분명히 다릅니다.

b라는 변수 값에 a의 메모리 주소 값을 넣었을 뿐이고, b라는 변수에는 자신만의 메모리 주소가 존재하죠.

a := 2
b := &a
fmt.Println(a, b)

이 코드로 출력한다면 2, 0x14000122008 a의 값과 a의 메모리 주소 값을 살펴볼 수 있습니다.

반대로 이 메모리 주소가 가지고 있는 값을 알아보는 방법도 있습니다.

*를 이용합니다.

b := &a
fmt.Println(*b)

출력 결과는 a의 값인 2를 출력하게 됩니다.

계속 복사본을 만드는 것은 원하지 않을 수도 있습니다.

이러한 메모리를 알아야하는 이유는 아주 무거운 데이터 구조를 다뤄야할 때, 메모리에 저장된 같은 Object(객체)를 원할 때 중요한 역할을 하는 것입니다.

이게 가장 중요한 부분인데

위에서는 b라는 변수에 a라는 값을 가지도록 선언했을 때 a값을 바꾸어도 b값은 변하지 않았습니다.

그렇지만 *포인터를 사용하게 되면 계속 변하는 값을 알아낼 수 있습니다.

a := 2
b := &a
a = 5
fmt.Println(*b)
5

b는 a의 메모리 주소값을 가지고 있고 이 메모리 주소가 가지고 있는 값이 변하면 *b는 현재 a의 값을 살펴볼 수 있습니다.

b는 a랑 연결되어있기 때문입니다.

더해서 b를 이용해 a의 값을 변경할 수도 있는 것이죠.

*b = 25 이렇게 하면 a를 출력하든 *b를 출력하든 같은 25를 출력합니다.

다시 한번 강조하지만 b의 변수 메모리 주소 &b값은 자신만의 고유한 주소를 가지고 있습니다. b를 출력하면 25가 나오거나 하지않고, 선언했을 때 그대로 해석하면

a변수의 메모리 주소인 &a값을 b변수에 담아두고 있기 때문입니다.


Arrays and Slices 배열

Go에서는 일반적인 배열 문법이 차이가 있습니다.

Go에서 배열은 다양하게 사용할 수 있습니다.

그리고 꼭 배열 인덱스 내부 값을 만들 필요는 없습니다.

코드의 예시를 살펴보자면..

func main() {
   arr := [5]string{"name", "age", "phoneNumber", "address", "company"}
   fmt.Println(arr)
}

또는

name := [5]string{}
fmt.Println(name)

이렇게 선언할 수 있습니다.

좀 더 다양한 방법도 많이 있습니다.

arr := []string{"name", "age", "phoneNumber"}
arr[3] = "lala"
arr[4] = "chiki"
arr[5] = "blah"

길이가 가변적으로 변할 수 있을 땐 인덱스의 길이를 정하지 않고 선언할 수도 있습니다.

arr := [5]string{"name", "age", "phoneNumber"}
arr[3] = "chiki"
arr[4] = "blah"

이렇게 쓴다던지,

arr := []string{"name", "age", "phoneNumber"}

fmt.Println(arr)

이렇게만 쓴다던지...

하지만, 사용할 수 없는 방식도 있습니다.

arr := [5]string{"name", "age", "phoneNumber"}
arr[3] = "chiki"
arr[4] = "blah"
arr[5] = "blah"
-----
error

길이는 5개로 지정되었는데 인덱스를 더 늘려서 값을 추가한다던지,

arr := []string{"name", "age", "phoneNumber"}
arr[3] = "chiki"
---------
error

길이를 지정하지 않았는데 인덱스를 추가해서 값을 넣는다던지..

배열을 조금 더 다양하게 사용해보도록 스스로 가지고 놀아보셔야 합니다.

어떻게 해야 되는지 안되는지를 알면 이 언어에서 배열은 어떻게 사용하라고 제시하는지를 익힐 수 있습니다.

그렇지만 분명 우리는 배열의 길이를 지정하지 않고도 추가할 수 있는 방법이 있습니다.

Javascript나 Python에서는

arr.push()

를 이용했으면 됐습니다. Go에서는 append()를 사용합니다.

func main() {
   arr := []string{"name", "age", "phoneNumber"}
   arr = append(arr, "chiki")
   fmt.Println(arr)
}

길이 값을 정하지 않고 원하는 값을 추가하고 싶을 때 append 함수를 사용합니다.


Map - 데이터 구조

Go에서 Map은 Javascript나 그 외 언어와 비슷하지만 약간은 다릅니다.

완전히 똑같지는 않습니다.

char 타입 형태의 map을 생성하기 위해서 해야할 것은 아래 문법입니다.

corner := map[string]string{"name":"corner", "age" : "28"}

map[key 자료타입]value 자료타입{ key : value }인데 key와 value의 자료타입이 다르게 넣을 수 없습니다.

// 잘못된 문법 
corner := map[string]string{"name" : "corner", "age" : 28}

이렇게 작성한다면 코드에서 오류라고 뜰 것입니다.

키 값은 string이라 문제 없지만, value를 string 타입으로 지정했는데 int 값을 넣었기 때문이죠.

map도 range를 이용해서 반복문을 작성할 수도 있습니다.

func main() {
	corner := map[string]string{"name": "corner", "age": "28"}
	for key, value := range corner {
		fmt.Println(key, value)
	}
	fmt.Println(corner)
}

그리고 알다시피 ignore도 가능합니다.

for _, value := range corner {
  fmt.Println(value)
}

Go에서 Map은 이게 전부라 많은 설명을 할 필요가 없을 것 같습니다.


Structs

{
   "name" : "corner",
   "age" : 28
   "skills": ["string"]
}

이러한 형태의 object를 가지고 싶다면 어떻게 해야할까요?

이걸 하기 위해서 struct를 사용해야 합니다.

struct는 structure같은 구조체입니다.

첫 번재로 어떤 struct인지 정의합니다.

먼저 main()함수 밖에다가

type user struct {
   name string
   age int
   skills []string
}

type 형 구조체를 정의합니다. 흡사 C언어 같죠?

구조체를 defined하는 것입니다.

그리고 main() 함수에서 출력을 하는데,

출력하는 방법 중 user() 함수에 인자값을 순서대로 넣어서 출력하는 방법이 있습니다.

skils := []string{"vue", "nuxt", "go", "java", "spring", "nodejs"}
corner := user{"corner", 28, skils}
fmt.Println(corner)

하지만 이렇게 사용한다면 실용성이 있을까요? 전혀 없을겁니다.

명확하게 보이지 않고, 좋아보이는 코드는 아닌 것 같군요.

corner := user{
   name:   "corner",
   age:    28,
   skills: skils,
}

이런식으로 쓴다면 일반적으로 썼던 js 방식과 흡사합니다.

그리고 명확하게 보이죠. 어떤 key에 value가 있는지..

Go는 class가 없습니다. struct가 class의 역할을 수행 하지만, method도 구조체로부터 분리되는 구성을 가지고 있습니다. 단일 상속도 없고 당연하게도 다중 상속도 없습니다. 객체 지향이 아니라 절차 지향인가 싶기도 하지만 충분히 객체 지향적인 언어입니다. 다른 방법으로 객체를 지향하고 있습니다.

  • class => struct 대안, 다른 OOP 클래스와는 달리 Non-Virtual(real) Method로만 구성됩니다.
  • receiver로 구조체와 함수를 연결해서 Method를 구현합니다.
  • 네임스페이스는 exports로 대신합니다.
  • 인터페이스(interface)로 다형성을 구현할 수 있습니다.
  • embedding으로 상속(extends)를 대신합니다. 객체 지향의 composition 모델과 비슷합니다.

위 코드를 class 키워드를 가지고 있는 언어로 프로그래밍 한다면....아마도

class user {
 	string name;
  int age;
	string skills[];
}

이런 느낌일겁니다.

즉, Java를 접해보셨더라면 (혹은 다른언어) Constructor method(생성자)가 없다는걸 단번에 알아 채셨거나, 이번에 깨닫게 되셨을겁니다.

python에서는 __init__ , js에서는 constructor()등등..

struct에는 생성자가 없다는 것만 알아두시면 됩니다.

Go에서 struct를 이해하는게 매우 중요합니다.

Go에서는 다른 프로그래밍 언어와 좀 차별되어 있기 때문에 이 부분을 조금 더 공부하고 연습하셔야 합니다.

profile
Full-stack Engineer. email - corner3499@kakao.com,

0개의 댓글