golang 스터디 5주차(슬라이스 / 메서드 / 인터페이스)

2023-11-26
14min
Golang

Chapter 18. 슬라이스

18.1 슬라이스의 선언 및 사용방법

18.1.1 슬라이스 선언

이전에 학습했었던 배열은 만들 때 정한 길이에서 더 이상 늘어나지 않는 문제가 있다. 따라서 기존에 생성한 배열에 값을 추가하고 싶다면 새로운 배열을 생성해야합니다.

슬라이스는 배열과 비슷하지만, 배열과는 다르게 개수를 적지 않고 생성할 수 있다.

go
// NOTE: 개수를 적지 않는다. 초기화를 명시적으로 하지 않으면 zero value 로 세팅된다. // NOTE: 그리고 length 는 0 이 된다. var slice []int // NOTE: 일반적인 초기화 slice2 := []int{1,2,3,4,5} // NOTE: 만약 특정 인덱스만 초기화하고, 나머지는 zero value 로 초기화할 때는 아래처럼 // NOTE: 주의할건 배열과 다르게 길이를 작성하지 않기 때문에 초기화 인덱스의 값에 의존해서 배열의 길이가 정해진다! slice3 := []int{1:10, 3:50} // [0, 10, 0, 50]``` // NOTE: make 를 이용한 초기화 slice4 := make([]int, 5) // NOTE: 슬라이스 순회 for _, v := range slice3 { fmt.Printf(v); }
go
// NOTE: 개수를 적지 않는다. 초기화를 명시적으로 하지 않으면 zero value 로 세팅된다. // NOTE: 그리고 length 는 0 이 된다. var slice []int // NOTE: 일반적인 초기화 slice2 := []int{1,2,3,4,5} // NOTE: 만약 특정 인덱스만 초기화하고, 나머지는 zero value 로 초기화할 때는 아래처럼 // NOTE: 주의할건 배열과 다르게 길이를 작성하지 않기 때문에 초기화 인덱스의 값에 의존해서 배열의 길이가 정해진다! slice3 := []int{1:10, 3:50} // [0, 10, 0, 50]``` // NOTE: make 를 이용한 초기화 slice4 := make([]int, 5) // NOTE: 슬라이스 순회 for _, v := range slice3 { fmt.Printf(v); }

18.1.4 슬라이스 요소 추가 - append()

슬라이스 챕터 처음부터 했던 말이 슬라이스는 배열과 다르게 추가가 가능하다고했다! append 를 사용하면 된다. golang's append 문서 를 보면 어떻게 사용해야하는지에 대해 상세하기 나와있다.

함수 시그니쳐를 보면 func append(slice []Type, elems ...Type) []Type 이다. Type 이라는건 잠시 넘어가고, []... 에 주목하자.

  • []Type 은 Type 의 슬라이스 타입을 의미하고
  • ...Type 은 Type 의 인자를 제한없이 받아들이겠다는 의미다. append 함수의 내부에서는 elems 의 타입이 []Type 로 변환된다.

따라서 append 함수를 아래처럼 사용하면 된다는 것을 알게 되었다.

go
arr := []int{1, 2, 3} // NOTE: arr 의 주소는 달라지지 않는다. append 함수는 반드시 return 을 받아야한다. arr = append(arr, 1, 2, 3) // NOTE: arr2 와 arr 은 주소가 다르다. arr2 := append(arr, 1, 2, 3)
go
arr := []int{1, 2, 3} // NOTE: arr 의 주소는 달라지지 않는다. append 함수는 반드시 return 을 받아야한다. arr = append(arr, 1, 2, 3) // NOTE: arr2 와 arr 은 주소가 다르다. arr2 := append(arr, 1, 2, 3)

그리고 return type 이 있다. 따라서 해당

INFO

참고로 Type 이 어떤 타입인지 보러가면 이렇게 아래처럼 나온다. 해석하면 type 에는 Go 의 모든 타입이 올 수 있지만, 같은 타입만 사용가능하다.

go
// Type is here for the purposes of documentation only. It is a stand-in // for any Go type, but represents the same type for any given function // invocation. type Type int
go
// Type is here for the purposes of documentation only. It is a stand-in // for any Go type, but represents the same type for any given function // invocation. type Type int

18.2 슬라이스 동작 원리

간단하게 말로 설명해보자면 슬라이스 구조체이고 3개로 이루어진다.

  • 포인터
  • len
  • cap

포인터는 배열의 주소를 가리키고 있다. len 은 그 포인터가 가리키고 있는 배열의 길이다. 중요한건 cap 이다. capacity 로 이 슬라이스가 가질 수 있는 최대의 길이를 의미한다. 만약 `append`` 로 최대 길이(cap) 이상의 배열이 되어야한다면 새로운 주소를 할당 받게 된다.

새로운 주소를 할당받는건 슬라이스 구조체의 포인터가 가리키는 주소.

아래의 코드를 보자. arrcap 이 3인 슬라이스다. 그리고 3개의 값이 zero value 로 채워져있으니 append 가 한번이라도 된다면 새로운 배열을 할당받게 되고, 슬라이스 구조체의 배열 포인터가 가리키는 주소가 달라지게 될 것이다.

  • &arrarr 변수의 주소.
    • 변함 없을 예정
  • &arr[0]arr 슬라이스 구조체의 포인터가 가리키는 주소
    • 주소가 달라질 예정
go
func main() { arr := make([]int, 3, 3) fmt.Printf("addredd of arr: %p\n", &arr) fmt.Printf("addredd of arr's 0: %p\n", &arr[0]) fmt.Println("-------- append 합니다. ------") arr = append(arr, 1, 2, 3) fmt.Printf("addredd of arr: %p\n", &arr) fmt.Printf("addredd of arr's 0: %p\n", &arr[0]) } // NOTE: 결과 // addredd of arr: 0x1400000c030 // addredd of arr's 0: 0x140000141e0 // -------- append 합니다. ------ // addredd of arr: 0x1400000c030 // addredd of arr's 0: 0x1400001c210 달라짐
go
func main() { arr := make([]int, 3, 3) fmt.Printf("addredd of arr: %p\n", &arr) fmt.Printf("addredd of arr's 0: %p\n", &arr[0]) fmt.Println("-------- append 합니다. ------") arr = append(arr, 1, 2, 3) fmt.Printf("addredd of arr: %p\n", &arr) fmt.Printf("addredd of arr's 0: %p\n", &arr[0]) } // NOTE: 결과 // addredd of arr: 0x1400000c030 // addredd of arr's 0: 0x140000141e0 // -------- append 합니다. ------ // addredd of arr: 0x1400000c030 // addredd of arr's 0: 0x1400001c210 달라짐

여기서 주의해야할 점이 있다. 만약 return 이 arr 이 아닌 다른 변수라면, arr 이 가리키는 포인터는 변화가 없다.

go
func main() { arr := make([]int, 3, 3) fmt.Printf("addredd of arr: %p\n", &arr) fmt.Printf("addredd of arr's 0: %p\n", &arr[0]) fmt.Println("-------- append 합니다. ------") arr2 := append(arr, 1, 2, 3) fmt.Printf("addredd of arr: %p\n", &arr) fmt.Printf("addredd of arr's 0: %p\n", &arr[0]) fmt.Printf("addredd of arr2: %p\n", &arr2) fmt.Printf("addredd of arr2's 0: %p\n", &arr2[0]) }
go
func main() { arr := make([]int, 3, 3) fmt.Printf("addredd of arr: %p\n", &arr) fmt.Printf("addredd of arr's 0: %p\n", &arr[0]) fmt.Println("-------- append 합니다. ------") arr2 := append(arr, 1, 2, 3) fmt.Printf("addredd of arr: %p\n", &arr) fmt.Printf("addredd of arr's 0: %p\n", &arr[0]) fmt.Printf("addredd of arr2: %p\n", &arr2) fmt.Printf("addredd of arr2's 0: %p\n", &arr2[0]) }

18.2.3 배열과 슬라이스의 동작 차이의 원인

원인은 간단하다. 배열은 배열이고, 슬라이스는 배열을 가리키는 포인터를 포함하는 구조체이다. 또한 Go 언어에서의 대입 연산은 복사로 동작한다. 따라서 배열을 어떤 변수에 대입하게 되면 배열 전체가 복사된다. 반면 슬라이스가 어떤 변수에 대입된다면 구조체가 복사된다. 여기서 차이가 복사 되는 슬라이스 구조체에는 포인터 가 복사가 된다. 배열 전체가 복사가 되지 않는다.

18.3 슬라이싱

슬라이싱은 배열의 일부를 집어내는 기능이다. array[start:end] 처럼 사용한다.

기본적인 사용방법

go
arr2 := arr[startIndex:lastIndex] // NOTE: 아래처럼 생략가능. 둘 모두 생략하면 처음부터 끝까지 arr2 := arr[:4] arr2 := arr[3:] arr2 := arr[:]
go
arr2 := arr[startIndex:lastIndex] // NOTE: 아래처럼 생략가능. 둘 모두 생략하면 처음부터 끝까지 arr2 := arr[:4] arr2 := arr[3:] arr2 := arr[:]

append 와 마찬가지로 arr2 와 arr 이 가리키는 배열의 주소는 동일하다. 만약 이 상황에서 arr 의 cap 이 넘어서는 만큼 append 를 하면 어떻게 될까?

결과는 arr 와 arr2 의 각 슬라이스에서 가라키는 배열의 주소가 달라지게 된다. arr2 는 처음과 변함없고, arr 은 처음과 다른 배열을 가리키게 됨. 아래 코드 스니펫으로 확인해보자.

go
func main() { arr := make([]int, 3, 3) fmt.Printf("\naddredd of arr: %p\n", &arr) fmt.Printf("addredd of arr's 0: %p\n", &arr[0]) fmt.Println("-------- slicing 합니다. ------") arr2 := arr[:] fmt.Printf("addredd of arr2: %p\n", &arr2) fmt.Printf("addredd of arr2's 0: %p\n", &arr2[0]) fmt.Println("-------- append 합니다. ------") arr = append(arr, 1, 2, 3, 4) fmt.Printf("addredd of arr's 0: %p\n", &arr[0]) fmt.Printf("addredd of arr2's 0: %p\n", &arr2[0]) }
go
func main() { arr := make([]int, 3, 3) fmt.Printf("\naddredd of arr: %p\n", &arr) fmt.Printf("addredd of arr's 0: %p\n", &arr[0]) fmt.Println("-------- slicing 합니다. ------") arr2 := arr[:] fmt.Printf("addredd of arr2: %p\n", &arr2) fmt.Printf("addredd of arr2's 0: %p\n", &arr2[0]) fmt.Println("-------- append 합니다. ------") arr = append(arr, 1, 2, 3, 4) fmt.Printf("addredd of arr's 0: %p\n", &arr[0]) fmt.Printf("addredd of arr2's 0: %p\n", &arr2[0]) }

Chapter. 19 메서드

메서드는 함수의 일종이다. Go 언어에는 클래스가 없는대신 구조체 밖에서 구조체에서 사용할 메서드를 지정한다. 구조체 밖에서 메서드를 정의할 때는 리시버라는 특별한 문법을 사용해야한다.

19.1 메서드 선언

메서드를 선언하려먼 리시버를 func 키워드와 함수 이름 사이에 명시해야함

아래 코드를 보면 int 라는 기본 타입에 MyReceiver 라는 이름을 붙였는데, 이걸 별칭 타입 이라고 한다.

go
type MyReceiver int func (r MyReceiver) info() int { return int(r) } func main() { var a MyReceiver = 10 b := a.helloworld() MyReceiver(b).helloworld() }
go
type MyReceiver int func (r MyReceiver) info() int { return int(r) } func main() { var a MyReceiver = 10 b := a.helloworld() MyReceiver(b).helloworld() }

19.3 포인터 메서드 vs 값 타입 메서드

golang 에서 모든 대입 연산자는 복사가 이뤄진다고 했다. 따라서 굉장히 배열에 대입 연산자가 사용되면 모든 배열이 복사 되기 때문에 성능에 좋지 않고, 때문에 그런 경우에 대해서는 배열보다는 슬라이스가 쓰였다.

리시버도 마찬가지로, 값 타입이 사용되면 전체가 복사가 되고 포인터가 사용되면 그 구조체의 주소를 직접 넘기게 된다. 값 타입이면 변경점이 적용되지 않을 테고 포인터가 사용되면 값이 바뀌게 된다.

go
type Receiver struct { arr []int value int } func (r Receiver) subValueR() { r.value -= 1 } func (r *Receiver) subPointerR() { r.value -= 1 } func (r Receiver) appendValueR() { r.arr = append(r.arr, 1) } func (r *Receiver) appendPointerR() { r.arr = append(r.arr, 1) } func main() { r := Receiver{ arr: []int{1, 2, 3}, value: 10, } fmt.Printf("r's value: %d\n", r.value) r.subValueR() fmt.Printf("r's value after value sub: %d\n", r.value) // 10. 값 변화 없음 r.subPointerR() fmt.Printf("r's value after pointer sub: %d\n", r.value) // 9. 값 변함 }
go
type Receiver struct { arr []int value int } func (r Receiver) subValueR() { r.value -= 1 } func (r *Receiver) subPointerR() { r.value -= 1 } func (r Receiver) appendValueR() { r.arr = append(r.arr, 1) } func (r *Receiver) appendPointerR() { r.arr = append(r.arr, 1) } func main() { r := Receiver{ arr: []int{1, 2, 3}, value: 10, } fmt.Printf("r's value: %d\n", r.value) r.subValueR() fmt.Printf("r's value after value sub: %d\n", r.value) // 10. 값 변화 없음 r.subPointerR() fmt.Printf("r's value after pointer sub: %d\n", r.value) // 9. 값 변함 }

궁금증 그러면 Receivcer 구조체의 슬라이스에 append 를 하면 어떻게 될까?

예상으로는 arr 변수자체는 리시버로 가면서 복사가 이뤄지지면 arr 슬라이스 구조체가 가리키는 포인터주소 자체는 동일하기 때문에 값 리시버와 포인터 리시버 차이가 없을것임

--> 틀렸다! 왜냐면 값 리시버라면 arr 도 새롭게 복사 되면서 리시버로 사용된 구조체의 arr 과 다르다. 즉, append 를 하면 새로운 배열이 생성되기 때문이다. 위 슬라이싱에 설명되어있다.

Chapter 20. 인터페이스

인터페이스란 구현을 포함하지 않는 메서드 집합이다. 구체화된 타입이 아닌 인터페이스만 가지고 메서드를 호출할 수 있기 때문에 추후 프로그램의 요구사항 변경 시 유연하게 대처할 수 있다.

덕 타이핑을 지원한다.

20.1 인터페이스

인터페이스를 이용하면 메서드 구현을 포함한 구체화된 객체가 아닌 추상화 된 객체로 상호작용 할 수 있다. --> 상호작용이라는 말이 조금 이상한데, 간단히 말하면 인터페이스 를 하나의 타입으로 보고 함수의 인자 타입 등으로 사용할 수 있다는 의미

20.1.1 인터페이스 선언

type 을 쓴 위 이름을 명시하고 interface 키워드로 마무리한다. type 을 사용하는 것에서 알 수 있다시피 interface 도 하나의 타입이다. 타입이긴 해도 이전 챕터까지에서 배운 구조 / 별칭 타입과는 분명히 다르다. 기존의 타입을 concrete type 라고도 한다.

go
type DuckInterface interface { Method() type }
go
type DuckInterface interface { Method() type }

20.3 덕 타이핑

Go 언어에서는 어떤 타입이 인터페이스를 포함하고 있는지 여부를 결정할 때 Duck typing 방식을 사용한다. 덕 타이핑 방식이란 타입 선언 시 해당 타입이 인터페이스 구현 여부를 명시적으로 나타낼 필요 없이, 인터페이스에 정의 된 메서드를 구현하기 만 하면 자동으로 해당 인터페이스 타입을 포함한한다고 결정하는것이다.

20.4 인터페이스 기능 더 알기

  • 인터페이스는 다른 인터페이스를 가질 수 있다.
  • empty 인터페이스 라고 하는 모든 타입이 포함하는 인터페이스가 존재한다.
  • 인터페이스의 기본값(zero value)은 nil 이다.

20.5 인터페이스 변환하기

인터페이스 변수를 타입 변환을 통해 구체화된 다른 타입이나 다른 인터페이스로 변환할 수 있다.

20.5.1 구체화된 다른 타입으로 변환하기

인터페이스 변수를 다른 타입으로 변환할 수 있다. 이 방법은 인터페이스를 본래의 구체화된 타입으로 복원할 때 주로 쓰인다. 방법은 아래와 같다.

go
var a Interface t := a.(Type)
go
var a Interface t := a.(Type)