Sad Puppy 3 [묘공단] Go언어 고급 18-19장(슬라이스, 메서드) :: 개발자 아지트

슬라이스

 

  • 동적 배열, 동적 배열이란 자동으로 배열 크기를 증가시키는 자료구조이다. ⇒ 관리 용이
  • 슬라이싱 기능을 이용해 배열의 일부를 나타내는 슬라이스를 만들 수 있음

 


사용법

 

아래와 같이 사용하면 고정된 크기의 배열을 사용하게됨.

var array [10]int

 

슬라이스 사용: 배열의 개수를 적지 않고 선언함.

-아래의 코드를 작성하면, 슬라이스를 초기화 하지 않은것이고, 길이가 0인 슬라이스가 만들어짐

이때, 슬라이스 길이를 초과해서 접근하면 런 타임 에러가 발생함

var slice []int

 

슬라이스 초기화 방법 1.

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]

 

슬라이스 초기화 방법 2. make() 내장 함수 사용.

var slice = make([]int, 3) // 타입, 배열의 길이, 각 요소 값은 int타입의 기본값인 0으로 채워짐

 

슬라이스 접근- C에서의 접근과 동일.

슬라이스 순회- 반복문을 통해서 순회함 len함수, range함수 사용

슬라이스 요소 추가- python에서 사용하는 append를 사용, 여러값 추가 가능


슬라이스 동작 원리

 

  • 슬라이스는 배열과 비슷하지만, 제대로된 동작 원리 이해가 필요함
  • 내장 타입이라 내부 구현이 감춰져 있음(reflect 패키지의 SliceHeader 구조체를 사용해 내부 구현을 살펴볼 수 있음)
  • 슬라이스 내부 정의는 다음과 같음

-배열을 가리키는 포인터

-배열 요소 개수를 나타내는 len

-전체 배열 길이를 나타내는 cap

필드로 구성된 구조체임

  • 슬라이스 변수 대입 시 배열에 비해서 사용되는 메모리나 속도에 이점이 있다. (?)

-make함수를 통해 슬라이스를 선언할 수 있음


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

 

동작 차이의 원인

-go 언어에서 모든 값의 대입은 복사로 일어남. (함수의 인수로 전달될 때, 다른 변수에 대입할 때 등)

 

복사는 타입의 값이 복사됨.

 

포인터는 포인터의 값인 메모리 주소가 복사됨

구조체는 구조체의 모든 필드가 복사됨

배열은 배열의 모든 값이 복사됨.

 

슬라이스의 경우, 슬라이스 내부의 배열을 가리키는 포인터, len, cap만 복사되므로, 실제 배열의 값을 복사하는건 아님 .


슬라이싱

 

-배열의 일부를 집어내는 기능

-슬라이싱을 이용하면 슬라이스를 반환함(즉, 잘린 부분)

 

array[startIndex:endIndex]// 이때 슬라이스는 array의 startIndex~endIndex-1까지 슬라이싱 값

 

 

슬라이싱은 슬라이스도 슬라이싱 할 수 있다.

 

슬라이싱시, startIndex 자리를 비워두면 0으로 인식한다.

슬라이싱시, endIndex 자리를 비워두면 특정 요소부터 끝 요소 까지 슬라이싱 한다.

startIndex, endIndex를 둘다 비워두면 대상을 전체에 대해 슬라이싱한다.

 

슬라이싱시, 인덱스 3개를 사용하면 cap의 크기를 조절할 수 있다.

마지막 인덱스를 사용하면 대상 배열의 전체 길이를 사용하는게 아닌, 마지막 인덱스까지만 배열을 사용할 수 있음

 

*배열이나 슬라이스 뒤에 …를 붙이면, 모든 요솟값을 넣어준 것과 같게 됨.

 

copy함수를 이용하여 슬라이스를 복사할 수 있다.


슬라이스 요소 삭제

 

슬라이스의 중간 요소를 삭제하기 위해서는 C 배열에서 특정 인덱스의 값을 삭제할 때 처럼, 삭제한 인덱스 이후의 인덱스에 대해 일일이 값을 당겨줘야 한다.

이때, append()함수를 사용하면 코드를 한줄로 이를 해결할 수 있다.

지우고자 하는 값의 인덱스만 제외하고 append함수를 통해 각 값들을 복사한다.


슬라이스 요소 추가

 

만약, 슬라이스 요소를 슬라이스 중간에 추가 하고자 하는 경우

  • append함수를 통한 슬라이스 맨 뒤에 요소 추가(임의의 값)
  • 맨 뒤 값부터 삽입하려는 인덱스까지의 요소들을 한 칸씩 뒤로 밀어줌(하나씩 뒤로 복사)
  • 삽입하려는 값을 삽입하고자 하는 인덱스로 이동시킨다.

이와 같은 과정을 append()함수를 중첩 사용한 코드 한줄로 사용 가능하다.


슬라이스 정렬

 

-Go 언어에서 기본 제공하는 sort 패키지를 통해 슬라이스를 정렬할 수 있다.

-sort 패키지는 python에서 사용하는 sort함수와 비슷하다.

 

import(
			"sort"
)
//s라는 int타입의 정렬되지 않은 임의의 슬라이스가 있다. 
sort.Ints(s) // 정렬 

// 만약 float타입의 슬라이스를 정렬하고자 한다면 
// Float64s()함수를 사용하면 된다.  

 

-Go에서는 구조체 슬라이스도 정렬할 수 있다.


메서드

 

메서드란?

-함수의 일종

-Go언어에는 클래스가 없는 관계로, 구조체 밖에 메서드를 지정함. 구조체 밖에 메소드를 정의 할때 리시버라는 기능을 사용함

 

리시버란?

-구조체 밖에 메서드가 있다. 따라서 해당 메서드는 어느 구조체에 속하는지 표시해야 하고, 이때 리시버를 사용한다.

즉, 리시버는 메서드가 속하는 타입을 알려주는 기법임.

 

효과는?

-메서드를 사용하여 데이터와 기능이 묶이니 응집도가 높아짐

-코드 재사용성 용이

-모듈화로 코드 가독성이 좋아짐

 

메서드 선언

 

func(r Rabbit) info() int { // r Rabbit은 리시버이고 소괄호로 표시함. info()는 메서드의 이름이다. 
	return r.width * r.height
}

 

 

info() 메서드는 Rabbit 타입에 속한다.

구조체 변수 r은 해당 메서드에서 매개변수처럼 사용된다.

리시버는 모든 로컬 타입(해당 패키지 안에서 type 키워드로 선언된 타입)이 가능함

따라서 패키지 내에 선언된 구조체 및 별칭 타입들이 리시버가 될 수 있다.

 

package main

import "fmt"

type accoutn struct {
	balance int
}

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

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

func main(){
	a:= &account{ 100 } // balnace가 100인 account 포인터 변수 생성
}

withdrawFunc(a, 30) // 함수 형태 호출

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

// 위의 함수와 메서드는 똑같은 역할을 하지만 형태가 다르므로, 호출 방법도 다르다.
// 메서드는 해당 리시버 타입에 속한다. 따라서 리시버를 점 연산자를 사용해 호출함

// 구조체 필드에 접근할 때 처럼 . 연산자를 사용해 해당 타입에 속한 메서드를 호출할 수 있다.

 

 

 

별칭 리시버 타입

 

별칭 리시버 타입은 별칭일 뿐 리시버가 될 수 있고, 메서드를 가질 수 있다.

Int와 같은 내장 타입들도 별칭 타입을 활용해서 메서드를 가질 수 있다.

기본 내장 타입들은 별칭 타입으로 변환하여 메서드를 선언할 수 있다.

package main

import "fmt"

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

// userInt 별칭 타입을 리시버로 갖는 메서드
func (a userInt) add(b int) int {
	return int(a) + b
}

func main() {
	var a userInt = 10
	fmt.Println(a.add(30))
	var b int = 20
	fmt.Println(userInt(b).add(50)) 
    // int 타입과 userInt 타입은 엄연히 다른 타입이므로 연산이 불가함 따라서
    // int 타입을 userInt 타입으로 형변환 하여 사용
}

 

 

메서드가 필요한 이유

 

함수도 있는데 왜 굳이 복잡하게 메서드라는걸 만든걸까?

 

함수와 메서드는 중요한 차이가 있다.

⇒ 소속 유무의 차이, 일반 함수는 어디에도 속하지 않지만, 메서드는 리시버에 속한다.

 

데이터와 관련된 기능을 메서드를 통해 구현하고 이를 데이터와 묶기 좋다.

따라서 코드 응집도를 높이는 중요한 역할을 한다. 좋은 코드는 결합도는 낮고 응집도는 높다. 

메소드는 해당 부분에 중요한 역할을 한다. 

 

코드 응집도가 떨어지면, 새 기능을 추가할 때 흩어진 모든 관련 부분을 검토하고 고쳐야 하는 문제가 발생한다.

코드 수정 범위가 늘어나면 관리는 복잡해지고, 실수가 발생할 확률이 높아지고, 따라서 버그를 만들 확률도 높아진다.

따라서 코드 응집도를 높일 필요가 있다.

 

객체지향 중심으로 변화

메서드가 등장함으로써, 메서드로 데이터와 기능을 묶을 수 있게 되고, 이것은 객체로 동작할 수 있게 됐다.

이는 절차 중심에서 객체 간 관계 중심으로 프로그래밍 패러다임이 변화를 일으켰다. 

과거에는 기능 호출 순서도를 나타내는 플로우차트를 중시했지만, 이제는 객체간 관계를 나타내는 클래스 다이어그램이 더 중요하게 됐다. 

 

객체란 데이터와 기능을 갖는 타입이고, 해당 객체의 타입의 인스턴스를 객체 인스턴스라고 한다. 

이런 객체 인스턴스들이 서로 상호작용을 함으로써, 객체 간의 관계 중심으로 프로그래밍 패러다임이 변화했다. 

(OOP, Object Oriented Programming)

 

포인터 메서드와 값 타입 메서드

리시버를 일반 값 타입과 포인터 값으로 정의할 수 있다. 

 

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) // 포인터 메서드 호출, mainA의 주솟 값이 a1으로 복사됨 
    // mainA와 a1은 같은 인스턴스를 바라봄
	fmt.Println(mainA.balance) // 70 출력

	mainA.withdrawValue(20) // 값 메서드 호출, mainA의 모든 값이 a2로 전달됨 
    // mainA와 a2는 서로 다른 인스턴스를 바라봄
	fmt.Println(mainA.balance) // 70 출력

	var mainB account = mainA.withdrawReturnValue(20) // mainB는 50
	fmt.Println(mainB.balance) // 값 메서드 값에서 변경된 값 반환 메서드 실행

	mainB.withdrawPointer(30) 
	fmt.Println(mainB.balance) // 20
}

 

메서드는 리시버의 타입을 그대로 따라가서 메서드는 리시버의 타입에 속함. 

포인터 메서드를 호출시, 포인터가 가리키는 메모리 주소값이 복사되고, 값 메소드 호출시, 리시버 타입의 모든 값이 복사됨. 

 

포인터 메서드는 메서드 내부의 리시버 값을 변경시킬 수 있음.

값 타입 메서드는 호출하는 쪽과, 메서드 내부의 값은 서로 다른 인스턴스이고, 따라서 메서드 내부 리시버 값을 변경시킬 수 없음

 

 

 

 

 

해당 글은 [Tucker의 Go 언어 프로그래밍] 18장~19장을 읽고 공부한 글입니다. 

+ Recent posts