Study

[스터디] Tucker의 Go 언어 프로그래밍 내용정리 18~20장

mokpolar 2023. 11. 24. 00:04
반응형

18장. 슬라이스

18.1 슬라이스

18.1.1 슬라이스 선언

  • 일반적인 배열은 처음 정한 길이에서 늘어나지 않는다.
var array [10]int
  • 위에서 10개 보다 많이 저장하려면 더 큰 배열을 만들어서 값을 하나씩 복사해야 한다.
  • 슬라이스의 선언 방법
  • 초기화 하지 않으면 길이가 0인 슬라이스가 만들어지고, 슬라이스길이를 초과하면 런타임에러가 발생
var slice []int // 배열의 개술를 적지 않는다. 
  • 할당되지 않은 메모리 공간에 접근해서 프로그램이 비정상 종료되는 패닉이 발생한다.
package main

import "fmt"

func main() {
	var slice []int
	
	if len(slice) == 0 { // slice 길이가 0인지 확인
		fmt.Println("slice is empty", slice)
	}
	
	slice[1] = 10 // 길이가 0이니데 두 번째 접근하려고 해서 패닉 발생
	fmt.Println(slice)
}

slice is empty []
panic: runtime error: index out of range [1] with length 0

goroutine 1 [running]:
main.main()
        /Users/krafton/Documents/GitHub/my-playground/tucker_golang/hello/ex18.1.go:12 +0x88
  • {}를 이용해 초기화
  • 5번째 요소가 2, 10번째 요소가 3
var slice1 = []int{1, 2, 3}
var slice2 = []int{1, 5:2, 10:3} // [1 0 0 0 0 2 0 0 0 0. 3]
var array = [...]int{1,2,3} // 배열 선언
var slice = []int{1, 2, 3} // 슬라이스 선언
  • make() 를 이용한 초기화
  • make 함수의 첫 번째 인수에는 만들고자 하는 타입, 두 번째 인수에는 길이
var slice = make([]int, 3)

18.1.2 슬라이스 요소 접근

  • 대괄호 사이에 인덱스를 써서 요소에 접근
var slice = make([]int, 3)
slice[1] = 5

18.1.3 슬라이스 순회

var slice = []int{1, 2, 3}

for i := 0; i < len(slice); i++ {
	slice[i] += 10
}

for i, v := range slice { // range로 각 요소를 순회할 수 있다. 첫 번째는 인덱스, 둘째 요솟값
	slice[i] = v * 2
}

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

package main

import "fmt"

func main() {
	var slice = []int{1, 2, 3}
	
	slice2 := append(slice, 4) 

	fmt.Println(slice)
	fmt.Println(slice2)

}

[1 2 3]
[1 2 3 4]

18.2.5 여러값 추가

  • append를 하면 슬라이스에 값들이 추가된 뒤 만들어진 새로운 슬라이스를 반환한다.
  • 기존 슬라이스에 추가하고 싶으면 기존 슬라이스에 대입해서 변경해야 한다.
slice = append(slice, 3, 4, 5, 6, 7)

18.2 슬라이스 동작 원리

  • reflect 패키지의 SliceHeader 구조체를 사용해 내부 구현을 살펴볼 수 있다.
  • 슬라이스 구현은 3 필드로 구성된 구조체이다.
    • Data uintptr : 실제 배열을 가리키는 포인터
    • Len int : 요소 개수
    • Cap int : 실제 배열의 길이
  • 슬라이스가 실제 배열을 가리키는 포인터를 가지고 있어서 쉽게 크기가 다른 배열을 가리키도록 변경할 수 있다.
  • 슬라이스 변수 대입 시 , 배열에 비해서 메모리나 속도에 이점이 있다.
type SliceHeader struct {
	Data uintptr
	Len int
	Cap int
}

 

 

18.2.1 make() 함수를 이용한 선언

  • len이 3이고, cap이 3이다.
  • 총 배열 길이가 3, 요소개수가 3
var slice = make([]int, 3)

  • 배열 길이는 5, 요소 개수는 3
  • 총 5개 중 3개만 사용하고 2개는 비워둔 경우
var slice2 = make([]int, 3, 5) 

18.2.2 슬라이스와 배열의 동작 차이

  • 아래의 경우 둘 다 배열과 슬라이스를 매개변수로 받아 3 번째 값을 200으로 변경한다.
  • 출력 결과를 보면 의도와 다르다.
package main

import "fmt"

func changeArray(array2 [5]int) { // 배열을 받아서 세 번째 값  변경
	array2[2] = 200
}

func changeSlice(slice2 []int) { // 슬라이스를 받아서 세 번째 값 변경
	slice2[2] = 200
}

func main() {
	array := [5]int{1, 2, 3, 4, 5}
	slice := []int{1, 2, 3, 4, 5}

	changeArray(array)
	changeSlice(slice)

	fmt.Println("array :", array)
	fmt.Println("slice :", slice)
}

array : [1 2 3 4 5]
slice : [1 2 200 4 5]

18.2.3 동작 차이의 원인

  • Go에서 모든 값의 대입은 복사로 일어난다.
    • 함수에 인수로 전달될 때
    • 다른 변수에 대입할 때
    • 값의 이동은 모두 복사이다.
  • 복사는 타입의 값이 복사된다.
  • 포인터는 포인터의 값인 메모리주소,
  • 구조체는 구조체의 모든 필드가 복사된다.
  • 배열은 배열의 모든 값이 복사된다.

 

 

 

 

  • array는 메모리 공간이 다른 새로운 배열로 복사된다.
  • slice가 slice2로 복사될 때는 구조체의 각 필드값이 복사되어, 똑같은 메모리 주솟값을 가지게 된다.

18.2.4 append()를 사용할 때 발생하는 예기치 못한 문제 1

  • append()함수가 호출되면 먼저 슬라이스에 값을 추가할 수 있는 빈 공간이 있는지 확인한다.
  • 남은 빈 공간은 실제 배열 길이 cap 에서 슬라이스 요소 개수 len 을 뺀 값이다.
  • 남은 빈 공간의 개수가 추가하는 값의 개수보다 크거나 같은 경우, 배열 뒷부분에 값을 추가한 뒤 len 값을 증가시킨다.
package main

import "fmt"

func main() {
	slice1 := make([]int, 3, 5)
	slice2 := append(slice1, 4, 5)

	// cap() 함수를 이용해 슬라이스 capacity 값을 알 수 있다.
	fmt.Println("slice1:", slice1, len(slice1), cap(slice2))
	fmt.Println("slice2:", slice2, len(slice2), cap(slice2))

	slice1[1] = 100 // slice2 값까지 바뀐다.

	fmt.Println("After change second element")
	fmt.Println("slice1:", slice1, len(slice1), cap(slice1))
	fmt.Println("slice2:", slice2, len(slice2), cap(slice2))

	slice1 = append(slice1, 500) // slice2 까지 바뀐다.

	fmt.Println("After append 500")
	fmt.Println("slice1:", slice1, len(slice1), cap(slice1))
	fmt.Println("slice2:", slice2, len(slice2), cap(slice2))
}

slice1: [0 0 0] 3 5
slice2: [0 0 0 4 5] 5 5
After change second element
slice1: [0 100 0] 3 5
slice2: [0 100 0 4 5] 5 5
After append 500
slice1: [0 100 0 500] 4 5
slice2: [0 100 0 500 5] 5 5

 

 

 

 

18.2.5 append()를 사용할 때 발생하는 예기치 못한 문제 2

  • 빈 공간이 없을 때 값을 추가하면 다른 경우가 생긴다.
  • append() 함수가 호출되면 먼저 빈 공간이 충분한지 확인한다.
  • 만약 충분하지 않으면 새로운 더 큰 배열을 마련한다.
  • 일반적으로 2배 크기가 된다. 그 다음 기존 배열의 요소를 모두 새로운 배열에 복사한다.
  • 그리고 새로운 배열의 뒤에서 새 값을 추가한다.
  • cap은 새로운 배열의 길이 값이 되고, len은 기존의 길이에 추가한 개수만큼 더한 값이 되고,
  • 포인터는 새로운 배열을 가리키는 슬라이스 구조체를 반환한다.

18.3 슬라이싱

  • 배열의 일부를 집어내서 슬라이스를 반환한다.
  • 끝 인덱스 -1 까지를 집어낸다.
  • 결과로 배열 일부를 가리키는데 , 새로운 배열이 만들어지는 게 아니라 배열의 일부를 포인터로 가리키는 슬라이스를 만들어낸다.
array[startIdx:endindex]
package main

import "fmt"

func main() {
	array := [5]int{1, 2, 3, 4, 5}

	slice := array[1:2] // 슬라이싱

	fmt.Println("array:", array)
	fmt.Println("slice:", slice, len(slice), cap(slice)) // 크기가 4이다. cap 길이는 array의 인덱스 1에서부터 배열의 마지막 인덱스까지의 길이를 갖게된다. 

	array[1] = 100 // array의 두 번째 값 변경

	fmt.Println("After change second element")
	fmt.Println("array:", array)
	fmt.Println("slice:", slice, len(slice), cap(slice))

	slice = append(slice, 500) // 슬라이스에 값 추가

	fmt.Println("After append 500")
	fmt.Println("array:", array)
	fmt.Println("slice:", slice, len(slice), cap(slice))

}

array: [1 2 3 4 5]
slice: [2] 1 4 
After change second element
array: [1 100 3 4 5]
slice: [100] 1 4
After append 500
array: [1 100 500 4 5]
slice: [100 500] 2 4

18.3.1 슬라이싱으로 배열 일부를 가리키는 슬라이스 만들기

  • 슬라이스는 배열을 가리키는 포인터, len, cap 필드로 구성되어 있으며,
  • 포인터는 값으로 메모리 주소를 갖기 때문에 배열의 중간을 가리킬 수 있다.
  • cap은 포인터가 가리키는 배열이 할당된 크기, 즉 안전하게 사용할 수 있는 남은 배열 개수를 나타낸다.
  • 배열의 총 길이에서 시작 인덱스를 밴 만큼 가지게 된다.

 

  • 빈 공간이 남아있으므로 array[2] 요솟값이 변경된다.

 

18.3.2 슬라이스를 슬라이싱하기

  • cap은 배열의 전체 길이에서 시작 인덱스를 뺀 값이 되지만,
  • 슬라이싱할 때 인덱스를 3개 사용해서 cap까지 조절할 수 있다.
slice[ 시작인덱스 : 끝인덱스 : 최대인덱스 ]

18.4 유용한 슬라이싱 기능 활용

18.4.1 슬라이스 복제

  • 같은 배열을 가리켜서 문제가 발생할 수 있는 문제를 해결하는 방법은 슬라이스를 복제하는 것이다.

 

 

package main

import "fmt"

func main() {
	slice1 := []int{1, 2, 3, 4, 5}
	slice2 := make([]int, len(slice1)) // slice1과 같은 길이의 슬라이스 생성
	
	for i, v := range slice1 { // slice1의 모든 요솟값 복사
		slice2[i] = v
	}

	slice1[1] = 100
	fmt.Println(slice1)
	fmt.Println(slice2)
}
	
  • 위 내용을 append 함수로 줄이면
slice2 := append([]int{}, slice1...}
  • 내장 함수 copy()를 사용하면
    • 첫 번째 인수로 복사한 결과를 저장하는 슬라이스 변수를 넣고
    • 두 번째 인수로 복사 대상이 되는 슬라이스 변수를 넣는다.
func copy(dst, src []Type) int
package main

import "fmt"

func main() {
	slice1 := []int{1, 2, 3, 4, 5}
	slice2 := make([]int, 3, 10) // len:3, cap:10 슬라이스
	slice3 := make([]int, 10)    // len:10, cap: 10 슬라이스

	fmt.Println("slice1:", slice1)
	fmt.Println("slice2:", slice2)
	fmt.Println("slice3:", slice3)

	cnt1 := copy(slice2, slice1) // slice1을 slice2에 복사, 슬라이스 길이 중 더 작은 개수만큼 복사
	cnt2 := copy(slice3, slice1) // slice1을 slice3에 복사

	fmt.Println(cnt1, slice2) // cnt는 실제로 복사한 요소 개수를 반환한다.
	fmt.Println(cnt2, slice3)
}

// slice1: [1 2 3 4 5]
// slice2: [0 0 0]
// slice3: [0 0 0 0 0 0 0 0 0 0]
// 3 [1 2 3]
// 5 [1 2 3 4 5 0 0 0 0 0]

18.4.2 요소 삭제

  • 슬라이스 중간 요소를 삭제하고 중간 요소 이후 값을 앞당긴다.
  • 그 뒤 맨뒤 값을 지운다.

 

package main

import "fmt"

func main() {
	slice := []int{1, 2, 3, 4, 5, 6}
	idx := 2; // 삭제할 인덱스

	for i := idx+1; i < len(slice); i++ { // 요소 앞당긴다. 
		slice[i-1] = slice[i]
	}

	slice = slice[:len(slice)-1] // 슬라이스로 마지막 값을 잘라준다. 

	fmt.Println(slice)

}
  • append() 함수 사용
    • 처음부터 idx 하나전까지 집은 슬라이스이며 지우고자 하는 인덱스 요소는 포함하지 않는다.
    • 하나 뒤의 값부터 끝까지 슬라이스 한다.
slice = append(slice[:idx], slice[idx+1:]...)

 

18.4.3 요소 추가

 

 

 

package main

import "fmt"

func main() {
	slice := []int{1, 2, 3, 4, 5, 6}
	
	slice = append(slice, 0) // 맨 뒤에 요소 추가

	idx := 2 // 추가 위치

	for i := len(slice)-2; i >= idx; i-- {
		slice[i+1] = slice[i] // 맨 뒤부터 추가하려는 위치까지 값을 하나씩 옮겨주기 
	}

	slice[idx] = 100 // 값 변경

	fmt.Println(slice)

}
  • append 함수를 사용하면
slice = append(slice[:idx}, append([]int{100}, slice[idx:]...)...)
  • 불필요한 메모리 사용이 없으려면
    • 임시 슬라이스는 불필요한 메모리가 사용되므로
slice = append(slice, 0) // 요소 하나 추가
copy(slice[idx+1:], slice[idx:]) // 한칸씩 밀려서 값 복사
slice[idx] = 100 // 남은 위치에 대입

18.5 슬라이스 정렬

18.5.1 int 슬라이스 정렬

package main

import (
	"fmt"
	"sort"
)

func main() {
	s := []int{5, 2, 6, 3, 1, 4}
	sort.Ints(s)
	fmt.Println(s)
}

 

 

19장 메서드

  • 메서드는 리시버를 func 키워드와 함수 이름 사이에 소괄호로 명시해야 한다.
  • info() 메서드이고
  • (r Rabbit) 이 리시버이다.
  • 구조체 변수 r은 해당 메서드에서 매개변수처럼 사용된다.
  • 리시버로는 모든 로컬 타입이 가능하다.
    • 로컬타입 : 해당 패키지 안에서 type 키워드로 선언된 타입.
    • 패키지 내 선언된 구조체. 별칭 타입들..
func (r Rabbit) info() int {
	return r.width * r.height
}
fpackage main

import "fmt"

type account struct {
	balance int
}

func withdrawFunc(a *account, amount int) { // 일반 함수 표현
	a.balance -= amount
}

func (a *account) withdrawMethod(amount int) { // 메서드 표현
	a.balance -= amount
}

func main() {
	a := &account{ 100 } // balance가 100인 account 포인터 변수 생성
	
	withdrawFunc(a, 30)  // 함수 형태 호출

	a.withdrawMethod(30)  // 메서드 형태 호출

	fmt.Printf("%d \\n", a.balance)
}

40 
  • 메서드 정의는 같은 패키지 내 어디에도 위치할 수 있다.
  • 리시버 타입이 선언된 파일 안에 정의하는게 일반적인 규칙이다.
  • 메서드는 해당 리시버 타입에 속한다.
  • 그래서 구조체의 필드처럼 . 연산자를 사용해 해당 타입에 속한 메서드를 호출할 수 있다.

19.1.1 별칭 리시버 타입

  • 별칭 타입도 리시버가 될 수 있다.
  • int 같은 내장 타입도 별칭 타입을 활용해서 메서드를 가질 수 있다.
package main

import "fmt"

type myInt int // 사용자 정의 별칭 타입

func (a myInt) add(b int) int {
	return int(a) + b
}

func main() {
	var a myInt = 10 // myInt 타입 변수
	fmt.Println( a.add(30) ) // myInt 타입의 add() 메서드 호출
	var b int = 20
	fmt.Println(myInt(b).add(50)) // int 타입을 myInt로 타입 변환
}

19.2 메서드는 왜 필요한가?

  • 함수와 다를게 뭔가
  • 소속이 다르다.
  • 메서드는 리시버에 속한다.
  • 그러니까 메서드를 사용해서 데이터와 기능을 묶을 수 있다.
  • 기능과 데이터를 묶어주는 역할, 즉 응집도를 높여주는 역할을 한다.

19.2.1 객체지향 : 절차 중심에서 관계 중심으로 변화

  • 객체는 데이터와 기능을 갖는 타입을 말하고, 이 타입의 인스턴스를 객체 인스턴스 object instance 라고 한다.
  • 이런 객체 인스턴스들이 서로 소통하고 관계를 맺음에 따라 객체 간 관계중심으로 프로그래밍 패러다임이 변화함.
  • Object Oriented Programming OOP
  • Go 언어에서는 클래스와 상속을 지원하지 않고, 메서드와 인터페이스만을 지원한다.

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

package main

import "fmt"

type account struct {
	balance   int
	firstName string
	lastName  string
}

// 포인터 메서드 : 리시버로 포인터를 갖는다.
// 포인터 메서드를 호출하면 포인터가 가리키고 있는 메모리의 주솟값이 복사된다.
func (a1 *account) withdrawPointer(amount int) {
	a1.balance -= amount
}

// 값 타입 메서드 : 리서버로 값 타입을 갖는다.
// 값 타입 메서드를 호출하면 리시버 타입의 모든 값이 복사된다.
func (a2 account) withdrawValue(amount int) {
	a2.balance -= amount
}

// 변경된 값을 반환하는 값 타입 메서드 : 리시버로 값 타입을 갖는다.
func (a3 account) withdrawReturnValue(amount int) account {
	a3.balance -= amount
	return a3
}

func main() {
	var mainA *account = &account{100, "Joe", "Park"}
	mainA.withdrawPointer(30)  // 포인터 메서드 호출
	fmt.Println(mainA.balance) // 70 출력

	mainA.withdrawValue(20)    // 값 타입 메서드 호출
	fmt.Println(mainA.balance) // 여전히 70 출력. 포인터가 아니라서?

	var mainB account = mainA.withdrawReturnValue(20)
	fmt.Println(mainB.balance) // 50 출력

	mainB.withdrawPointer(30)  // 포인터 메서드 출력
	fmt.Println(mainB.balance) // 20 출력
}

 

  • mainA 포인터 변수의 값, 즉 메모리 주솟값만 a1으로 복사된다. 

 

 

  • withdrawPointer() 메서드가 호출되면 mainA 포인터 변수가 갖는 값 , 즉 메모리 주소가 복사되기 때문에 
  • a1과 mainA는 같은 인스턴스를 가리킨다. 
  • 그래서 withdrawPointer() 메서드 내부에서 a1의 balance 를 변경하면 mainA도 같은 인스턴스를 가리키기 때문에 mainA의 balance도 변경된다. 
  • a1과 mainA는 같은 인스턴스를 가리킨다. 

 

 

  • mainA의 모든 값, 즉 account 구조체의 balance, firstName, lastName 모두 a2로 복사된다. 
  • 호출되는 과정에서 mainA의 모든 내용이 복사되기 때문에 withdrawValue() 내의 a2 변수와 main() 함수내의 mainA 변수는 서로 다른 메모리 주소를 가지게 된다. 
  • mainA와 a2는 서로 다른 인스턴스이다. 

 

  • 그래서 withdrawValue() 메서드에서 a2의 balance를 변경해도 mainA의 balance는 변경되지 않는다. 
  • 이를 해결하려면 withdrawReturnValue() 메서드처럼 변경된 값을 다시 반환해야 한다. 

 

  • account 구조체의 모든 값이 메서드 호출 시와 결괏값 반환시 두 번 복사된다. 
  • mainB는 메서드 호출 이후 변경된 값을 갖는 새로운 객체를 나타낸다. 

 

  • a3, mainA, mainB 모두 다른 메모리를 주소로 갖는 서로 다른 객체이다. 

 

 

 

 

 

20. 인터페이스

20.1 인터페이스

  • 인터페이스 interface , 메서드 구현을 포함한 구체화된 객체 concrete object가 아닌 추상화된 객체로 상호작용할 수 있다.

20.1.1 인터페이스 선언

  • 인터페이스도 구조체처럼 타입 중 하나이기 때문에 type을 써줘야 한다.
  • 변수 선언이 가능하고, 변수의 값으로 사용할 수 있다.
type DuckInterface interface {
	Fly()
	Walk(distance int) int
}
  • 3가지 의무사항
    1. 메서드는 반드시 메서드 명이 있어야 한다.
    2. 매개변수와 반환이 다르더라도 이름이 같은 메서드는 있을 수 없다.
    3. 인터페이스에서는 메서드 구현을 포함하지 않는다.
type Sample interface {
	String() string
	String(int) string // 에러: String 메서드명이 겹친다. 
	_(x int) // 에러 : 메서드는 반드시 이름이 있어야 한다. 
package main
import "fmt"

type Stringer interface { // Stiringer 인터페이스 선언
	String() string // 매개변수 없이 string 타입을 반환하는 메서드 포함
} // 이제 매개변수 없이 string타입을 반환하는 String() 메서드를 포함한 모든 타입은
// Stringer 인터페이스로 사용 가능. 

type Student struct {
	Name string
	Age  int
}

func (s Student) String() string { // Student 의 String() 메서드
	return fmt.Sprintf("안녕 ! 나는 %d살 %s라고 해", s.Age, s.Name)
}

func main() {
	student := Student{ "철수", 12 } // Student 타입
	var stringer Stringer // Stringer 타입
	
	stringer = student // stringer 값으로 student 대입 Student는 String() 메서드 포함

	// stringer 인터페이스 갖고 있는 String() 메서드 호출
	fmt.Printf("%s\\n", stringer.String()) 
}

20.2 인터페이스 왜 쓰나?

  • 인터페이스를 이용하면 구체화된 객체가 아닌 인터페이스만 가지고 메서드를 호출할 수 있다.
  • 큰 코드 수정없이 필요에 따라 구체화된 객체를 바꿔서 사용할 수 있다.
  • 아래와 같이 Sender 인터페이스를 사용한다.
    • Sender인터페이스는 Send() 메서드만 포함하고
    • SendBook() 함수는 Sender 인터페이스를 입력으로 받는다.
    • koreaPost, fedex 모두 Send(string) 메서드를 갖고 있으므로 Sender 인터페이스로 이용가능하며 SendBook() 함수의 인수로 사용할 수 있다.
  • 이렇게 하면 메서드 내부 구현을 알 필요 없이 코드를 유연하게 사용할 수 있다.
// Fedex 에서 제공한 패키지

package fedex

import "fmt"

// Fedex에서 제공한 패키지 내 전송을 담당하는 구조체
type FedexSender struct {
}

func (f *FedexSender) Send(parcel string) {
	fmt.Printf("Fedex sends %v parce\\n", parcel)
}
// 우체국에서 제공한 패키지

package koreaPost

import "fmt"

// 우체국에서 제공한 패키지 내 전송을 담당하는 구조체
type PostSender struct {
}

func (k *PostSender) Send(parcel string) {
	fmt.Printf("우체국에서 택배 %v를 보냅니다.\\n", parcel)
}
package main

import (
	"tucker_golang/ch20/fedex"
	"tucker_golang/ch20/koreaPost"
)

type Sender interface {
	Send(parcel string)
}

func SendBook(name string, sender Sender) {
	sender.Send(name)
}

func main() {
	// 우체국 전송객체, Fedex 전송 객체 모두 SendBook 인수로 활용 가능
	koreaPostSender := &koreaPost.PostSender{}

	SendBook("어린 왕자", koreaPostSender)
	SendBook("그리스인 조르바", koreaPostSender)

	// Fedex 전송 객체를 만든다.
	fedexSender := &fedex.FedexSender{}
	SendBook("어린 왕자", fedexSender)
	SendBook("그리스인 조르바", fedexSender)
}

우체국에서 택배 어린 왕자를 보냅니다.
우체국에서 택배 그리스인 조르바를 보냅니다.
Fedex sends 어린 왕자 parce
Fedex sends 그리스인 조르바 parce

20.2.1 추상화 계층

  • 추상화 : 내부 동작을 감춰서 서비스 제공, 사용자 측 모두에게 자유를 주는 방식
  • 인터페이스 : 추상화를 제공하는 추상화 계층

20.3 덕타이핑

  • 덕타이핑 : 타입 선언시 인터페이스 구현여부를 명시적으로 나타낼 필요 없이 인터페이스에 정의한 메서드 포함여부만으로 결정
  • 아래와 같이 별다른 명시없이 String() 메서드를 포함한 것만으로 Stringer 인터페이스로 사용할 수 있다.
type Stringer interface {
	String() string
}

type Student struct {
...
}

func (s *Student) String() string {
...
}

20.4 인터페이스 기능 더 알기

20.4.1 인터페이스를 포함하는 인터페이스

  • 인터페이스도 다른 인터페이스를 포함할 수 있다.
type Reader interface {
	Read() (n int, err error)
	Close() error
}

type Writer interface {
	Write() (n int, err error)
	Close() error
}

// Read(), Write(), Close() 메서드를 가지게 된다. 
type ReadWriter interface {
	Reader // Reader의 메서드 집합을 포함한다.
	Writer // Writer의 메서드 집합을 포함하낟.
}

20.4.2 빈 인터페이스 interface{}를 인수로 받기

  • interface{} 는 메서드를 가지고 있지 않은 빈 인터페이스이다.
  • 그래서 모든 타입이 빈 인터페이스로 쓰일 수 있다. 어떤 값이든 받을 수 있는 함수, 메서드, 변숫값을 만들 때 사용한다.
  • 아래의 경우 PrintVal() 함수 인수로 빈 인터페이스인 interface{}를 받고, 빈 인터페이스이기 때문에 모든 타입을 인수로 쓸 수 있다.
package main

import "fmt"

func PrintVal(v interface{}) {
	switch t := v.(type) { // v의 타입에 따라 다른 로직을 수행한다.
	case int:
		fmt.Printf("v is int %d\\n", int(t))
	case float64:
		fmt.Printf("v is float64 %f\\n", float64(t))
	default:
		//그 외 타입인 경우 타입과 값을 출력한다.
		fmt.Printf("Not supported type: %T:%v\\n", t, t)
	}
}

type Student struct {
	Age int
}

func main() {
	PrintVal(10)
	PrintVal(3.14)

	PrintVal(Student{15})
}
v is int 10
v is float64 3.140000
Not supported type: main.Student:{15}

20.4.3 인터페이스 기본값 nil

  • 인터페이스 변수의 기본값은 유효하지 않은 메모리 주소를 나타내는 nil 이다.
package main

type Attacker interface {
	Attack()
}

func main() {
	var att Attacker // 기본값 nil, 초깃값이 없어서 기본값이 된다. 
	att.Attack()  // att가 nil 이기 때문에 런 타임 에러가 발생한다. 
}

<aside> 💡 컴파일 타임에러와 런 타임 에러 컴파일 타임 에러 : 코드를 기계어로 전환하며 실행 파일로 만드는 중에 발생한 에러, 문법 오류 등 런 타임 에러 : 실행 도중 예기치 않은 문제로 발생하는 에러. 문법에 문제가 없으나 값이 비정상인 경우

</aside>

20.5 인터페이스 변환하기

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

  • 인터페이스를 다른 구체화된 타입으로 타입 변환할 수 있다.
  • 인터페이스를 본래의 구체화된 타입으로 복원할 때 주로 사용한다.
  • 인터페이스 변수 뒤에 점. 을 찍고 소괄호()안에 변경하려는 타입 써주기
var a Interface
t := a . (ConcreteType)

 

 

반응형