Go/Go Basic

Effective Go 02

guiwoo 2023. 2. 9. 23:57

출처 : https://go.dev/doc/effective_go#arrays

Arrays

C와 Go Array의 주요 차이점

1. Array 는 값이다. 하나의 배열을 다른 배열에 할당하면? 전체복사 가 발생된다.

2. 만약 함수변수로 사용한다면 이건 포인터 타입이 아닌, 카피 값이 넘어간다.

3. 배열의 크기는 하나의 타입이다. 

위 3가지를 직접 증명해 보자.

- Array는 값이다. 하나의 배열을 다른 배열에 할당하면? 전체복사 가 발생된다. 

func main() {
	arr := [3]int{1, 2, 3}
    arr2 := [3]int{}
	arr2 = arr
	fmt.Printf("Type is %T, Point is %p, Values %v\n", arr, &arr, arr)
	fmt.Printf("Type is %T, Point is %p, Values %v\n", arr2, &arr2, arr2)
}

결괏값 

Type is [3] int, Point is 0x1400012e018, Values [1 2 3]
Type is [3]int, Point is 0x1400012 e030, Values [1 2 3]

 

다른 배열에 할당하면 이와 같이 전체복사가 발생된다. 메모리 주소의 생성이 보는 바와 같이 4바이트 int 값 3개 총 12바이트 늘어난 e030부터 시작되는 것도 재밌는 포인트인 것 같다.

- 만약 함수변수로 사용한다면 이건 포인터 타입이 아닌, 카피 값이 넘어간다.

 

func main() {
	arr := [3]int{1, 2, 3}
	func(arr [3]int) {
		fmt.Println("In Function")
		fmt.Printf("Type is %T, Point is %p, Values %v\n", arr, &arr, arr)
	}(arr)
	fmt.Printf("Type is %T, Point is %p, Values %v\n", arr, &arr, arr)
}

결괏값

In Function
Type is [3] int, Point is 0x140000ac030, Values [1 2 3]
Type is [3]int, Point is 0x140000 ac018, Values [1 2 3]

 

보는 바와 같이 익명함수는 arr [3] int 를인자로 받는 함수이다. 거기에 기존에 선언한 arr를 넘겨주었지만 보는 바와 같이 다른 주소값을 반환하게 된다. 다시 말해 arr는  포인터 값이 아니기 때문에 레퍼런스 복사가 아닌 값복사가 발생되어 새로운 값을 할당하는 것이다.

- 배열의 크기는 하나의 타입이다. 

배열의 크기 자체가 타입이라는 말은 단순하게 그냥 위에서 선언한 함수에서 인자로 [5] int를 받는다고 하면 바로 컴파일 에러가 발생한다. 

위에서 말한 이유를 생각한다면 저렇게 받아야만 한다 왜? 값 복사가 일어날 때 메모리 낭비, 데이터 유실 이 되지 않으려면 정확하게 계산된 메모리 값 주소를 할당해주어야 하기 때문이다. 

 

저 문서에서 값을 가지는 속성 또한 매우 유용할 수 있으나 비싸다고 한다. 그래서 만약 c와 같은 방식으로 구현하고 싶다면 go에서도 가능하다

func main() {
	arr := [3]int{1, 2, 3}
	func(arr *[3]int) {
		fmt.Println("In Function")
		fmt.Printf("Type is %T, Point is %p, Values %v\n", arr, &arr, arr)
	}(&arr)
	fmt.Printf("Type is %T, Point is %p, Values %v\n", arr, &arr, arr)
}

결괏값

Type is *[3] int, Point is 0x14000120018, Values &[1 2 3]
Type is [3]int, Point is 0x1400012 e018, Values [1 2 3]

주소값을 넘기고 포인터 타입으로 받으면 손쉽게 해결 가능하다. 그러나 이러한 구현 방식은 고 에 어울리지 않는다고 한다. 이에 대한 해결책으로 슬라이스를 제시하는데 바로 가보자.

Slices

슬라이스는 가장 보편적이고 강력하며 편리한 데이터의 연속적인 인터페이스라고 설명한다. 통상 인터페이스{} 이렇게 하면 모든 타입을 소화할 수 있는 마법의 키워드이다. 문서에서도 대부분의 내장라이브러리 의 배열 관련 된 부분은 모두 슬라이스로 처리한다고 한다.

읽다 보면 엄청 강조하는 부분 중 하나가 바로 슬라이스는 레퍼런스를 홀드 한다고 한다. 즉 배열과 달리 레퍼런스를 홀드 하게 되면 함수 인자 혹은 선언 시에 값 복사가 아닌 레퍼런스 참조를 하게 된다는 의미이다. 

그래서 위에 고 에 구현 방식에 어울리지 않는다고 하는데 한번 확인해 보자.

func main() {
	fmt.Printf("Type is %T, Point is %p and arr[0] Point is %p  value are %v \n", arr, &arr, &arr[0], arr)
	fmt.Printf("Type is %T, Point is value for %p and arr[0] Point is %p, value are %v \n", arr2, &arr2, &arr2[0], arr2)
}

Type is []int, Point is 0x1400000c030 and arr[0] Point is 0x1400001c090  value are [1 2 3] 
Type is []int, Point is value for 0x1400000c048 and arr[0] Point is 0x1400001c090, value are [1 2 3]

주의해서 볼 점은 여기서 arr2의 주소값은 물론 arr와 다르다 다만 arr2 [0]의 주소값을  보면 바로 arr [0]의 주소값을 가리킨다 다시 말해 아래 등호가 성립한다.

주소 arr2!= 주소 arr  / 주소 arr [0] == arr2 [0]

이렇게 되면 당연히 복사가 일어나지 않기 때문에 위 배열과 같은 포인터 타입과 주소의 작업을 하지 않아도 된다.

위의 특징 외에도 슬라이스 하면 길이 없는 배열을 가장 먼저 떠올리게 된다. 이걸 가능하게 해주는 append 함수에 대해서  문서에서 준 예시를 보자.

func Append(slice, data []byte) []byte {
    l := len(slice)
    if l + len(data) > cap(slice) {  // reallocate
        // Allocate double what's needed, for future growth.
        newSlice := make([]byte, (l+len(data))*2)
        // The copy function is predeclared and works for any slice type.
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0:l+len(data)]
    copy(slice[l:], data)
    return slice
}

기본적인 슬라이스를 선언해서 기존 사이즈 + 현재 들어오는 데이터 *2 만큼의 사이즈를 늘려주는데 자바의 어레이 리스트 내부 로직 중 grow()와 매우 유사하다. copy(목적지, 소스)인 형태이다 카피 구현 설명에 가보면 소스를 목적지로 오버래핑 한다고 되어있다.

추가적으로 인트 리턴 값이 있으나 이는 변경된 요소의 개수이다. 

다시 말해 사이즈를 늘리는 하나의 새로운 슬라이스를 만들고 그에 현재 요소들을 복사해 넣어주고 리턴해주는 방식이다. 그래서 보통 append 빌트인 함수 사용여부를 볼 때 리턴 받은 슬라이스를 대입하곤 한다.

이렇게 되면 당연히 기존 slice의 주소가 바뀌게 된다. 

다시 본다면 기존 포인팅 하던 주소가 바뀌는 걸 의미한다.

func main(){
	arr := make([]int, 3)
	arr[0] = 1
	fmt.Printf("%p %p\n", arr, &arr[0])
	arr = append(arr, 1, 2, 3, 4, 5, 6, 7, 8)
	fmt.Printf("%p %p\n", arr, &arr[0])
}

0x1400012e018 0x1400012e018
0x1400010e060 0x1400010e060

 

결과 값이 보는 바와 같이 주소가 다르다. 

추가적으로 슬라이스는 레퍼런스를 참조한다. 다시 말해 여러 다른 슬라이스에서 접근가능하다는 소리이다. 이는 다시말해 스레드 세이프 하지 않은 문제를 야기한다. 

이 왕같이 포인팅 되게 되면? 모든 곳에서 접근하여 슬라이스 요소를 수정할 수 있다 이는 다시 말해 스레드 세이프 하지 않다는 의미이다. 

아직 문서에서 언급은 없으나 뒷장에서 이에 대해 언급하면 그때 이문제 의 해결법과 트레이드오프에 대해 적어볼까 한다.

자바에서는 이문제를 해결하기 위해 synchronize 키워드와 클래스 하나를 제공해 주는 것으로 기억하고 있는데 어떻게 해결할지 궁금하다.

 

Maps

자바에서 가장 좋아하는 자료구조 중에 하나였다. 고에서도 똑같이 제공해 주며 해쉬맵 형태의 맵을 제공한다.

당연히 키 벨류에는 어느 타입이나 들어갈 수 있으나, 슬라이스는 맵의 키로 지정할 수 없다. 저 문서에는 어렵게 작성해 놨는데 슬라이스는 동등성을 제공하지 않는다.

func main(){
	arr := []int{1,2,3}
    arr2 := []int{1,2,3}
    fmt.Println(arr == arr2) // false
}

벨류를 가져오는 방법으로는 m [KEY]를 하면 VALUE를 리턴하게 된다.

또한 고 의 맵에서 특이한 점으로는 없는 벨류값에 대해 0을 리턴한다. 다시 말해 자바의 contains 같은 함수를 제공하지 않기 때문에 

```java```
class Main(){
	public static void main(){
    	Map<Integer,Integer> map = new HashMap();
        //code .. //
        if(map.conatians("찾는값"){
        	// blarblar
        }
    }
}

```go```

func main(){
	m := make(map[int]int)
    //code .. //
    if v,ok := m["찾는값"]; ok {
    	//blarblar
    }
}

이와 같은 방식으로  자바와 유사하게 로직을 규현 할 수 있다.

추가적으로 delete(map instance, key)를 작성하면 map 안에 있는 요소를 지울 수 있다. 맵 안의 그 벨류가 없더라도 특별한 에러 없이 진행되는데 그냥 사용한다면 버그를 유발할 수 있을 것 같다. 

delete 사용 전에 항상 유무를 체크하고 넘기는 것에 대해 고민해봐야 할 것 같다. 

Append

뭔가 이 문서의 순서가 중구난방인거 같다. 갑자기 print 쪽에 대해 설명하다가 갑자기 append 로 넘어온다.

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

append() 빌트 인 함수 정의 이다. T 는 제네릭 으로 어느 타입이나 받을수 있는것을 의미한다.

실제 함수에서는 T 를 사용할수 없다 이는 호출자에 의해 결정되는 사항이다.

그래서 T 를 컴파일러의 도움을 받아 정의하기에 빌트인 함수인것이다.

 

...T 를 보면 2번쨰 파라미터 로 다량의 데이터를 넘기는것이 가능하다. 다시말해 슬라이스 를 통으로 넘겨도 된다는 의미이다.

타입이 맞으면 x =  append(x,y...) 이런식으로