[GO] #2-4. 고랭 기본문법 (데이터)

Study·2021년 5월 18일
0

고랭

목록 보기
5/18
post-thumbnail

포인터 ( Pointer )

GO 는 포인터를 지원하며 포인터는 값의 메모리 주소를 가진다.

T 타입은 T 값을 가리키는 포인터이다. 이것의 zero valuenil 이다.

var p *int

& 연산자는 이것의 피연산자에 대한 포인터를 생성한다.

i := 42
p = &i

* 연산자는 포인터가 가리키는 주소의 값을 나타낸다.

fmt.Println(*p)	// 포인터 p를 통해 i 읽기
*p = 21		// 포인터 p를 통해 i 설정

이것은 역 참조 또는 간접 참조로 알려져 있다.

C 언어와 다르게, GO는 포인터 산술을 지원하지 않는다.

구조체

GO 는 포인터와 동일하게 구조체도 지원한다.

구조체는 필드의 집합체이며 다음과 같이 사용할 수 있다.

type Vertex struct {
	X int
	Y int
}

구초체의 필드는 .(dot) 연산자로 접근할 수 있다.

type Vertex struct {
	X int
	Y int
}
func main() {
	v := Vertex{1, 2}
	v.X = 4
	fmt.Println(v.X)	// 결과 4
}

구조체 포인터 를 통해서도 구조체 필드를 접근할 수 있다.

(*p).X 로 작성하면, 구조체 포인터 p 에서 구조체의 X 필드에 접근할 수 있다.

그러나 위 표기법은 번거로울 수 있다. 따라서 이 언어는 역 참조를 명시할 필요 없이 p.X 로 작성할 수 있다.

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

구조체 리터럴

구조체 리터럴은 필드 값을 나열하여 새로 할당된 구조체 값을 나타낸다.

접두사 &구조체 값으로 포인터를 반환한다.

var (
	v1 = Vertex{1, 2}  // 타입 Vertex 를 가짐
	v2 = Vertex{X: 1}  // Y:0 을 생략
	v3 = Vertex{}      // X:0 그리고 Y:0
	p  = &Vertex{1, 2} // 타입 *Vertex 를 가짐
)

배열

[n]T 타입은 타입이 T 인 n 값들의 배열이다.

var a [10]int

위 선언은 변수 a 를 10개의 정수들의 배열로 선언한 것이다.

배열의 길이는 그 타입의 일부이며, 배열의 크기를 조절할 수 없다.

Slices

배열은 고정된 크기를 가진 반면, 슬라이스는 배열의 요소들을 동적인 크기로 유연하게 볼 수 있다.

실제로, 슬라이스는 배열보다 훨씬 흔하다.

[ ]T 타입은 T 타입을 원소로 가지는 슬라이스이다.

슬라이스는 콜론으로 구분된 두 개의 인덱스(하한과 상한)를 지정하여 형성된다.

a [low : high]

이것은 첫 번째 요소를 포함하지만 마지막 요소를 제외하는 범위를 선택한다.

아래 표현은 a 의 인덱스 1 부터 네 번째 요소를 포함하는 슬라이스를 생성한다.

a [1 : 4]

슬라이스는 어떠한 데이터도 저장할 수 없으며 단지 배열의 한 영역을 나타낼 뿐이다.

슬라이스의 요소를 변경하면 배열의 해당 요소가 수정된다.

아래 예시는 배열을 공유하는 다른 슬라이스는 이런 변경사항을 확인할 수 있다.

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]

슬라이스 리터럴

슬라이스 리터럴은 길이가 없는 배열 리터럴과 같다.

아래는 배열 리터럴이다.

[3] bool {true, true, false}

이렇게 하면 위와 동일한 배열이 생성되고, 이를 참조하는 슬라이스가 만들어진다.

[] bool {true, true, false}

슬라이스 길이와 용량

상한 또는 하한을 생략하면 슬라이싱 시 기본 값을 사용할 수 있다.
하한의 경우 기본값 0, 상한의 경우 슬라이스의 길이이다.

var a [10] int

위 배열에서 아래 슬라이스 표현식은 모두 동일하다.

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

슬라이스는 _length (길이)__capacity (용량)_ 를 둘다 가지고 있다.

슬라이스의 길이는 슬라이스가 포함한 요소의 개수, 용량은 첫 번째부터 계산하는 요소의 개수이다.

슬라이스 s 의 길이와 용량은 len(s)cap(s) 식으로 얻을 수 있다.

슬라이스 길이 연장은 충분한 용량이 있다면 다시 슬라이싱 하면 된다.

func main() {
	s := []int{1, 2, 3, 4, 5}
	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 = 5, cap = 5 [1 2 3 4 5]
len = 0, cap = 5 []
len = 4, cap = 5 [1 2 3 4]
len = 2, cap = 3 [3 4]

nil 슬라이스

슬라이스의 zero valuenil 이다.

nil 슬라이스의 길이와 용량은 0 이며, 기본 배열을 가지고 있지 않다.

make 함수로 슬라이스 생성

내장 함수 make 로 슬라이스를 생성할 수 있다.

make 함수는 0 으로 이루어진 배열을 할당한다. 그리고 배열을 참조하는 슬라이스를 반환한다.

a := make([]int, 5)	// len(a) = 5

용량을 지정하려면, make 함수의 세 번째 인자에 값을 전달한다.

b := make([]int, 0, 5) // len(b)=0, cap(b)=5

b = b[:cap(b)] // len(b)=5, cap(b)=5
b = b[1:]      // len(b)=4, cap(b)=4

슬라이스의 슬라이스

슬라이스는 다른 슬라이스를 포함하여 모든 타입을 담을 수 있다.

func main() {
	// Create a tic-tac-toe board.
	board := [][]string{
		[]string{"_", "_", "_"},
		[]string{"_", "_", "_"},
		[]string{"_", "_", "_"},
	}

	// The players take turns.
	board[0][0] = "X"
	board[2][2] = "O"
	board[1][2] = "X"
	board[1][0] = "O"
	board[0][2] = "X"

	for i := 0; i < len(board); i++ {
		fmt.Printf("%s\n", strings.Join(board[i], " "))
	}
}

위 결과는 다음과 같다.

X _ X
O _ X
_ _ O

슬라이스 요소 추가

GO 는 내장된 append 함수를 제공한다.

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

append 의 첫 번째 파라미터는 s 는 슬라이스의 타입 T 이다.
그리고 나머지 T 값들을 슬라이스에 추가할 값들이다.

append 의 결과 값은 원래 슬라이스의 모든 요소와 추가로 제공된 값들을 포함하는 슬라이스이다.

만약 s 의 원래 배열이 너무 작아서 주어진 값을 모두 추가할 수 없을 경우, 더 큰 배열이 할당된다.
이때 반환된 슬라이스는 새로 할당된 배열을 가리킨다.

Range

for 에서 range 는 슬라이스 또는 맵의 요소들을 순회한다.

슬라이스에서 range 를 사용하면, 각 순회마다 두 개의 값이 반환된다.
첫 번째는 인덱스, 두 번째는 인덱스 값의 복사본이다.

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

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
for _, value := range pow

만약 인덱스만을 원하면, 두 번째 변수를 생략할 수 있다.

for i := range pow

Maps

맵은 키를 값에 매핑한다.

맵의 zero valuenil 이며, nil 맵은 키도 없고, 키를 추가할 수도 없다.

make 함수는 주어진 타입의 초기화되고 사용 준비가 된 맵을 반환한다.

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"])
}

위 결과

{40.68433 -74.39967}

맵 리터럴은 구조체 리터럴과 같지만 키가 필요하다.

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

최상위 타입이 타입 이름일 경우 리터럴 요소에서 생략할 수 있다.

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

Map 이용

m 맵에 요소를 추가하거나 업데이트 하기

m[key] = elem

요소 검색하기

elem = m[key]

요소 제거하기

delete(m, key)

두 개의 값을 할당하여 키가 존재하는지 테스트

elem, ok = m[key]

위 코드에서 keym 안에 있다면, oktrue 아니면 false

만약 key 가 맵 안에 없다면, elem 은 map 요소 타입의 zero value 이다.

실제 값에 상관없이 map 내부에 존재 여부만 필요할 경우, 공백 식별자 를 이용하여 변수가 있을 자리에 놓으면 된다.

_, ok = m[key]

함수

1. 함수 값

함수들도 값이며, 다른 값들과 마찬가지로 전달될 수 있다.

함수 값은 함수의 인수나 반환 값으로 사용될 수 있다.

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.Print(compute(math.Pow))
}

2. 함수 클로저

GO 함수들은 클로저일 수 있다.
클로저는 함수의 외부로부터 오는 변수를 참조하는 함수값이다.

함수는 참조된 변수에 접근하여 할당될 수 있다.
이런 의미에서 함수는 변수에 바운드 된다.

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

func main() {
	pos := adder()
	for i := 0; i < 10; i++ {
		fmt.Println(
			pos(i)	// sum 이 유지된 상태로 계속 더해짐
		)
	}
}

각 클로저는 그 자체의 sum 변수에 바운드되어 있다.

위 adder 함수는 익명함수 func() int 를 리턴하는 함수이다.
그리고 이 익명함수는 바깥의 sum 변수를 바운드하고 있는데, 익명함수 자체가 로컬 변수 sum 이 아니기 때문에 외부 변수 sum 이 값이 증가되는 것을 유지되고 있다.

클로저를 사용하면 위의 변수 pos 로 함수를 호출할 때마다 계속 가져다 쓸 수 있다.
즉, 함수가 선언될 때의 환경을 계속 유지한다.

이 외에는

1. New

내장 함수로 메모리를 할당하지만 다른 언어에 존재하는 같은 이름의 기능과는 다르게 메모리를 초기화하지 않고, 단지 값을 제로화(zero) 한다.

다시 말해, new(T) 은 새로 제로값이 할당된 타입 T 를 가리키는 포인터를 반환하는 것이다.

type SyncedBuffer struct {
    lock    sync.Mutex
    buffer  bytes.Buffer
}

p := new(SyncedBuffer)  // type *SyncedBuffer
var v SyncedBuffer      // type  SyncedBuffer

생성자와 합성 리터럴

때론 제로값만으로 충분하지 않고 생성자(constructor)로 초기화할 필요가 있다.
다음과 같은 예제를 확인하자.

func newRect(width, height int) *Rect {
	if width < 0 || height < 0 {
		return nil
    	}
	r := new(Rect)
	r.width = width
	r.height = height
    	return r
}

이 예제에는 불필요하게 반복되는 코드가 있다.
합성 리터럴 (앞서 배운 구조체 리터럴) 로 간소화하여 새로운 인스턴스를 만들어보자.

func newRect(width, height int) *Rect {
	if width < 0 || height < 0 {
		return nil
    	}
	r := Rect{width, height}
    	return r
}

합성 리터럴의 필드들은 순서대로 배열되고 반드시 입력해야 한다.
하지만, 요소들에 레이블을 붙여 필드 : 값 형태로 짝을 만들면,
초기화 순서에 관계 없이 나타날 수 있다.

입력되지 않은 요소들은 각자의 제로값을 가진다.

제한적으로, 합성 리터럴이 전혀 필드가 없을 경우에는
new(Type)&Type{ } 과 동일하다.

2. append

내장 함수 append 를 이용하면 슬라이스의 끝에 요소를 붙이고 결과를 반환할 수 있다.

형식은 다음과 같다.

func append(slice []T, elements ... T) []T

다음의 예제를 살펴보자.

x := []int{1,2,3}
x = append(x, 4, 5, 6)
fmt.Println(x)

위 결과는 [1 2 3 4 5 6] 을 출력한다.

그런데 append 를 이용하여 slice 에 slice 를 붙이고 싶다면 어떻게 해야할까?

쉬운 방법으로는 ... 를 이용하는 것이다.

아래 예제는 위와 동일한 결과를 산출한다.

x := []int{1,2,3}
y := []int{4,5,6}
x = append(x, y...)
fmt.Println(x)

... 이 없다면 컴파일되지 않는다 왜냐면 y 는 int 타입이 아니라 틀리기 때문이다.

profile
Study

0개의 댓글