2020/05/02 - [Coder/Go] - [Go]Mac os Go lang 개발환경 세팅 (with VScode)
1. 기본 자료구조
오늘은 기본 자료구조에 대해 소개해드리겠습니다. Python에서도 자주 만나는 List, map, Array 등이 Go에서는 어떻게 표현되는지, 그리고 Go가 가지고 있는 장점 중 하나인 Low level programming에 대해 얕게 알아봅니다.
1) 선언
기본적으로 go에서는 2가지의 선언 방식을 활용합니다. 바로 var
와 const
, type
입니다.
var는 variable 즉, 변수입니다. 함수 밖에서 선언해 전역 변수로 활용이 가능합니다. 위 예시에서 "number"라는 값은 숫자(int)의 값을 갖는 변수로 선언되어 있습니다. 선언과 동시에 값을 지정할 수 도 있고, 필요한 시점에 호출하여 값을 할당할 수 도 있습니다.
필요한 시점에 호출해 값을 지정할 수 있는 것은 go가 var이 선언되었을 때 값이 없다면 기본값으로 자동으로 할당해 기억해두기 때문입니다. ( ex. int는 0, string은 "")
var number, number2 int
// 다양한 값을 지정하여 마지막에 한번에 형식을 지정할 수 있습니다. 이 경우 number = number2 = 0 입니다.
var hour int = 21
// 이 처럼 선언 당시에 값을 할당하는 것도 가능합니다. 이 경우 hour = 21 입니다.
만약 특정 함수 내에서 변수를 선언해야 할 때는 어떻게 해야 할까요? 위처럼 전역으로 사용할 때는 반드시 var를 사용해야 하지만 함수 내에서는 아래와 같은 2가지 방법이 모두 가능합니다.
func variable() {
var number int = 10
numbers := 10
// 두 값 모두 10이라는 int값이 할당된 변수입니다.
}
변수(var)는 값을 변경할 수 있었습니다. 상수(const)는 정의하면 바꿀 수 없는 값입니다. Auth value 등 항상 고정된 값을 상수로 선언하는 경우가 많습니다. 사용법은 동일합니다.( := 는 사용이 불가능합니다!)
const workingday int = 5
const (
job = "Data Scientist"
difficuly = 10
// 타입을 지정하지 않아도 자동으로 추론합니다.
)
2) Array And Slice
python을 공부하신 분이라면 배열과 리스트랑 비슷한 친구들로 생각하시면 되겠습니다. go의 array는 양이 반드시 정해져 있는 구조입니다. 반면에 slice는 양이 정해져 있지 않습니다. 사용법은 동일하게 []type{data}
형태입니다.
slice는 정말 자주 사용하게 되는 구조이기 때문에 익숙해지는 게 좋습니다.
func main() {
// array and slice
array := [7]string{"london", "reny", "carrot", "jung", "keno"}
slice := []string{"london", "reny", "carrot", "jung", "keno"}
fmt.Println("array : ", array)
fmt.Println("slice : ", slice)
}
array : [london reny carrot jung keno ]
slice : [london reny carrot jung keno]
위와 같이 array는 빈 공간이 표시되지만 slice는 꽉 찬 형태로 표시되는 것을 알 수 있습니다.
그렇다면 append 등 구조에 값을 추가하거나 변형하는 경우는 어떻게 할까요? 이 경우 slice를 활용하는 게 좋습니다. 코딩을 하다 보면 어떤 값을 얼마나 추가할지, 변형하게 될지 알 수 없기 때문입니다.
newslice := append(slice, "go는 slice에 값을 추가하는 경우 기존 slice를 업데이트하지 않고,
동일한 slice 타입으로 복사합니다.")
fmt.Println(newslice)
[london reny carrot jung keno go는 slice에 값을 추가하는 경우 기존 slice를 업데이트하지 않고,
동일한 slice 타입으로 복사합니다.]
3) Map
map은 python의 dictionary랑 같다고 생각하시면 됩니다. map[key type]var type{key:var}
형태입니다.
slice, array와 다르게 각각의 값이 index를 갖고 있는 상태라고 생각하시면 됩니다. 따라서 특정 key값 또는 value값을 따로 혹은 동시에 호출하는 것이 가능합니다. 단 key와 value의 타입은 선언할 때 지정한 타입과 동일해야 합니다.
//map
maps := map[int]string{1: "london", 2: "reny", 3: "jung", 4: "carrot", 5:"keno"}
fmt.Println(maps)
for key, value := range maps {
fmt.Println(key, value)
}
map[1:london 2:reny 3:jung 4:carrot 5:keno]
1 london
2 reny
3 jung
4 carrot
5 keno
4) Struct
struct는 말 그대로 모든 구조를 담을 수 있는 상자 같은 존재입니다, map이나 slice, array의 경우 지정한 타입 이외에 복합적인 값을 가진 구조를 만들 수 없는데, struct가 그 부분을 해소해줍니다. 처음에 변수 선언에서 언급했지만 보여드리지 않았던 type이 여기서 등장합니다.
func main() {
// struct
type personinfo struct {
name string
age int
hobby []string
isgraduate bool
}
personInfo := personinfo{
name: "london",
age: 27,
hobby: []string{"basketball", "coding"},
isgraduate: true}
personInfo2 := personinfo{"london", 27, []string{"beer", "chicken"}, true}
fmt.Println(personInfo)
fmt.Println(personInfo2)
}
type을 통해 "personinfo"는 [string, int, [string] slice, bool] 값을 갖는 strcut(구조)로 정의해주기만 하면 끝입니다. 당연히 const, var로도 선언할 수 있으며, var, const처럼 전역으로 선언하는 것도 가능합니다. 다만 struct 자체가 특별한 목적을 가지고 만드는 경우가 많기 때문에 같은 구조가 반복해서 등장하는 경우가 아니라면 저는 필요할 때마다 선언해서 사용하는 것을 선호하는 편입니다.
2.Low Level Programming
go는 메모리단에 접근할 수 있는(심지어 쉬운) 몇 안 되는 언어입니다. (즉 low level programming이 가능한 언어입니다.)
얼마나 쉽냐면 어지간히 복잡한 메모리 분배/정리/제거/할당/호출 등은 몇 가지 연산자를 함께 몇 줄안에 끝낼 수 있습니다.(go가 가지는 직관성과 깔끔한 코드도 한몫합니다.)
간단하게 메모리에 할당되어 있는 값을 확인하고 호출하는 작업을 예로 들어 보겠습니다.
func main() {
// low level programming ( memory look up)
a := 2
b := a
c := &a
}
a := 2, b := a 로 각각 변수를 선언하였습니다. 이때 c는 b와 다르게 a앞에 & 가 붙어 있는 것을 볼 수 있습니다.
이 & 연산자가 바로 메모리의 주소를 호출하는 방법입니다. 즉, 현재 &연산자가 붙은 변수가 메모리의 어느 위치에 기록되어 있는지 알 수 있습니다.
그럼 memory에 저장된 값을 보기 위해서는 어떻게 해야 할까요? *연산자를 붙여주면 해당 메모리에 저장되어 있는 값을 볼 수 있습니다.
func main() {
// low level programming ( memory look up)
a := 2
b := a
c := &a
fmt.Println("a,b를 출력했을 때는 : ", a, b)
fmt.Println("둘의 메모리 주소를 보고싶을 때 : ", &a, &b)
fmt.Println("c에 a의 메모리 주소를 바로 넣으면 출력은 : ", a, c)
fmt.Println("메모리를 그대로 읽는 방법도 있습니다 :", *c)
}
a,b를 출력했을 때는 : 2 2
둘의 메모리 주소를 보고싶을 때 : 0xc0000140c8 0xc0000140d0
c에 a의 메모리 주소를 바로 넣으면 출력은 : 2 0xc0000140c8
메모리를 그대로 읽는 방법도 있습니다 : 2
만약 이 상태에서 a의 값이 바뀌면 어떻게 될까요? a = 1000을 입력해보겠습니다.
a = 1000
fmt.Println("결과는", "\n a :", a, "\n b :", b, "\n c : ", c, *c "\n a,b가 할당된 memory는 : ", &a, &b)
결과는
a : 1000
b : 2
c : 0xc0000140c8 1000
a,b가 할당된 memory는 : 0xc0000140c8 0xc0000140d0
a에는 새로 정의된 1000이라는 값이 저장되고, b는 기존에 a = 2 인 상태의 값으로 정의되어 있기 때문에 값이 변하지 않습니다. 또한 b가 저장된 메모리의 위치도 변하지 않습니다.
c는 어떨까요? c는 a가 들어있는 메모리 자체를 보고 있습니다. 따라서 b와 다르게 새로 정의된 a(즉 메모리에 현재 저장되어 있는 값) 1000을 갖게 됩니다.
이때 "**c를 a의 pointer" **라고 표현합니다. 간단하게 메모리에 직접 손을 댈 수 있는 특별한 상태를 지니고 있다고 이해하시고 이러한 포인터들을 통해 메모리 관리를 용이하게 할 수 있다는 것을 기억하시면 됩니다.
한 가지 예를 더 보여드리면 c는 a의 pointer이기 때문에 c를 변화시키는 것만으로도 a도 자동으로 같이 변화합니다.
a에 이미 1000이라는 값이 할당되었고, a를 변화시키지 않았는대도 c를 변화시키니 a가 같이 변한 것을 확인하실 수 있습니다.
*c = 50
fmt.Println("a의 메모리 주소 : ", &a, "c가 보고 있는 메모리 주소 : ", c, "로 a의 메모리 주소를 보고 있다는 것을 알 수 있습니다.")
fmt.Println("*c = 50 으로 메모리 주소의 값을 직접 바꿔주자 a의 값도", a, "으로 변한 것을 볼 수 있습니다.")
a의 메모리 주소 : 0xc0000140c8 c가 보고 있는 메모리 주소 : 0xc0000140c8 로 a의 메모리 주소를 보고 있다는 것을 알 수 있습니다.
*c = 50 으로 메모리 주소의 값을 직접 바꿔주자 a의 값도 50 으로 변한 것을 볼 수 있습니다.