본문 바로가기
Golang/Tucker

[묘공단] 3주차 (1)

by 윤원용 2023. 10. 15.

이 글은 골든래빗 《Tucker의 Go 언어 프로그래밍》의 09~15장 써머리입니다.

 

이번주는 배열(array)과 구조체(struct)를 중점으로 리뷰할 예정이다. 책에서는 너무 이론적인 부분들만 다루고 있는 것 같아 응용하는 예제를 만들어 리뷰할 예정이고 밑에 목차를 보면 포인터와 문자열은 간략하게 리뷰하겠다. 

더보기
  • 12 배열
    • 12.1 배열
    • 12.2 배열 사용법
    • 12.3 배열은 연속된 메모리 (부분 pass)
    • 12.4 다중 배열 pass
  • 13 구조체
    • 13.1 선언 및 기본 사용
    • 13.2 구조체 변수 초기화
    • 13.3 구조체를 포함하는 구조체
    • 13.4 구조체 크기
    • 13.5 프로그래밍에서 구조체의 역활
  • 14 포인터
    • 14.1 포인터란?
    • 14.2 포인터는 왜 쓰나?
    • 14.3 인스턴스
    • 14.4 스택 메모리와 힙 메모리
  • 15 문자열 (통으로 pass)
    • 15.1 문자열
    • 15.2 문자열 순회
    • 15.3 문자열 합치기
    • 15.4 문자열 구조
    • 15.5 문자열은 불변이다

예제는 링크드리스트(linkedList), 스택(stack), 큐(queue) 자료구조를 이용할 것이고 예제를 이해하기 위해서는 구조체(struct)와 포인터(pointer)를 확실히 이해해야 한다. 참고로 책과 다른 부분이 있는데 제너릭(generic)과 13 챕터 구조체에는 없고 19 챕터에 나오는 메서드에 대해 예습해야 한다. 

 

12.1 배열

배열은 동일한 타입의 원소(일단 값이라 생각해도 좋다.)를 연속적 메모리 공간에 첨자(index)를 통해 접근하여 원소를 저장하거나 불러올 수 있는 자료구조이다. 그러므로 배열의 메모리를 할당받기 위해서는 연속적인 메모리 공간의 길이를 운영체제에 알려줘야 길이만큼 연속적인 메모리 공간을 할당받을 수 있다. 

 

12.2 배열 사용법

배열 타입의 변수를 선언하는 방법은 이전에 리뷰했던 방법을 그대로 사용한다. 

var arr [5]int64 = [5]int64 { 1, 2, 3, 4 , 5 } // 명시적
arr = [5]int64 { 1, 2, 3, 4 , 5 } // 묵시적
arr = [...]int64 { 1, 2, 3, 4, 5 } // 배열의 길이는 모르겠고 원소 개수 만큼 할당 생성해줘~

위 코드에서 명시적 방법을 이해하기 쉽게 풀어보면 arr라는 명을 갖는 배열 변수를 선언하기 위해 운영체제에게 int64 타입의 연속적인 메모리 공간 길이가 5라고 알려주고 1부터 5까지 int64 타입의 요소들로 초기화하는 코드이다. 참고로 초기화를 안 하면 타입의 기본 값으로 길이만큼 초기화된다.

 

그럼 arr 배열 변수의 요소 중 3이라는 값을 갖는 요소를 읽기 위해 어떻게 해야 할까? 바로 첨자(index)를 사용해야 한다.

fmt.Println(arr[2] == 3)

여러 프로그래밍 언어에서는 첨자(index)를 사용하기 위해서는 대괄호 사이에 배열에서 접근하고 싶은 첨자(index)를 넣는 방법을 사용한다. 여기서 "3이라는 값은 세 번째에 있는데 왜 첨자 2로 접근하는 것일까?"라고 의문을 가질 수 있는데 컴퓨터는 효율을 중시하기 때문에 1부터 시작하면 0을 사용하지 않으므로 비효율적이라 0부터 시작해서 배열의 길이(지금은 5) - 1까지로 접근할 수 있게 만들어져 있다. 예제의 배열 크기를 기준으로 [0, 5) 이런 식으로 표현할 수 있는데 대괄호는 포함한다는 뜻이고 소괄호는 제외한다는 뜻이라 0~4까지 사용할 수 있다는 뜻이다.

fmt.Println(arr[5])

위 코드를 실행하면 어떤 결과가 나올까?

index out of bound exception

위 이미지처럼 배열에 할당받은 메모리 외에 위치한 메모리에 접근하려 하기 때문에 운영체제한테 혼난다. 실무에서도 종종 나오는 이슈임으로 꼭꼭 암기!!!

 

이제 생성한 배열을 이전에 리뷰했던 for문을 이용해 첨자(index)와 요소를 출력해 보면 

for index, element := range arr {
    fmt.Printf("index: %d, element: %d\n", index, element)
}

이렇게 첨자(index)가 0부터 시작하고 0번 첨자(index)에 있는 요소의 값은 1이다는 것을 알 수 있다.

 

12.3 배열 복사

Golang에는 list를 대신하는 slice라는 자료구조를 내장 api로 제공하기도 하고 배열의 길이를 변수를 통해 변경할 수 없기 때문에 (상수는 가능함) 큰 의미가 없어 pass

 

13 구조체, 14. 포인터

구조체와 포인터를 같이 리뷰하는 이유는 본인은 구조체와 포인터를 나눠 설명하기 어렵다 생각하기 때문이다. 책에선 구분 지어 놨는데 Golang에서는 거의 모든 자료형에 포인터를 사용할 수 있기 때문에 설정하기 유용하게 나눈 것 같다.

 

구조체를 쉽게 설명하면 여러 타입의 변수(field)들과 동작(method)을 하나의 이름으로 규결시 켜 사용할 수 있다. 책에서는 구조체에 여러 변수들이 있을 때 크기 계산하기와 컴퓨터가 효율적으로 메모리를 할당할 수 있도록 메모리 패딩 문제에 대해서도 설명해주고 있지만 임베디드 프로그래밍이 아니고서야 본인은 교양이라 생각하기 때문에 리뷰는 pass

 

구조체를 선언하기 위해서는 type과 struct라는 키워드가 꼭 필요하고 이전에 리뷰한 함수와 마찬가지로 첫 문자가 대문자면 외부 패키지에서도 접근할 수 있고 소문자면 접근할 수 없다. 위에 말한 field와 method도 똑같은 규칙이 적용된다.

type Node struct {
}

위처럼 선언하면 외부 패키지에서도 접근이 가능하고

type node struct {
}

위처럼 선언하면 외부 패키지에서 접근이 불가능하다.

 

struct를 생성하는 방법은 크게 두 가지가 있는데 첫 번째는 포인터로 생성하기고 두 번째는 값으로 생성하기다. 프로그램의 목적에 따라 struct를 포인터로 만들어 사용하거나 값으로 만들어 사용하기 때문에 코딩할 때 차이점을 잘 생각하고 선택하는 게 중요하다.

 

첫 번째 방법으로 포인터로 만들기다.

var node1_1 *Node = new(Node) // 명시적 1번 방법
var node1_2 *Node = &Node{} // 명시적 2번 방법 
node2_1 := new(Node) // 묵시적 1번 방법
node2_2 = &Node{} // 묵시적 2번 방법

위 코드처럼 다양한 방법이 있는데 new라는 내장 api를 사용해 처음부터 포인터로 만드는 방법이 있고 참조(&) 연산자를 사용해 값 형태를 포인터로 만들 수 있다. 각 방식의 차이는 field들을 어떻게 초기화하냐에 차이가 있는데 new 내장 함수를 사용해 만든 경우 instance의 method를 통해 field들을 초기화하는 방식이 있고 &연산자를 사용해 만드는 경우는 만들면서 초기화시킬 수 있기 때문에 어떤 방법이 좋다고 말하기보다 차이점을 이해하고 목적에 맞게 사용하면 좋을 것 같다.

 

예제 코드를 보기 전에 포인터에 대해 설정하자면 포인터는 *연산자를 사용하고 기본 값은 nil이다. 실무에서도 종종 nil에 접근(.) 연산자를 사용하여 서버가 죽는 아주 잘 못된 프로그래밍을 할 때도 있다.

null pointer excepion

서버가 죽어서 서버 컴퓨터에 들어가 로그를 확인 중 저런 예외가 발생했는데 콜스택이 본인이 짠 코드면 사수분께 달려가 죄송하다 할 때 뺨 맞아도 할 말이 없기 때문에 주의해야 한다.!!!! (본인도 이런 실수를 했는데 사수분이 천사라 살았다... 사랑합니다 사수님!!!)

 

LinkedList와 다른 예제에서 사용할 Node struct로 예제코드를 사용하면

var node1_1 *Node = new(Node) // 명시적 1번 방법
node1_1.prev = nil
node1_1.value = "Hello"

var node1_2 *Node = &Node{ // 명시적 2번 방법
	prev:  node1_1,
	value: " World",
}

node2_1 := new(Node) // 묵시적 1번 방법
node2_2 := &Node{}   // 묵시적 2번 방법

node1_1.next = node1_2
node1_2.next = node2_1

node2_1.init(node1_2, 1992, node2_2)
node2_2.init(node2_2, 1107, nil)

fmt.Printf("%p %p %p %p\n", node1_1, node1_2, node2_1, node2_2)

위 코드를 설명하면 node1_1 변수는 접근연산자를 사용해 field를 하나하나 초기화가 아닌 대입을 하고 node1_2 변수는 선언과 동시에 field들을 초기화해주고 있다. node2_1 변수와 node2_2 변수는 init method를 사용하여 field들의 값을 대입하는 형식으로 했다.  이렇듯 어떤 방법들이 있는지 이해한 후 목적에 맞게 구현하면 될 것 같다.

 

두 번째 방식으로 값으로 만들기다.

예제 코드와 간단하게 설명하는 것으로 넘어가겠다.

type Node struct {
	value interface{}
}

func main() {
	var node1 Node
	fmt.Println(node1.value)
	var node2 Node = Node{}
	node3 := Node{}

	fmt.Printf("%#+v %#+v %#+v\n", node1, node2, node3)
}

위 코드에서 node1을 초기화하지 않았는데 바로 아래 코드에서 node1 변수에 접근연산자를 이용해 value field를 접근하고 있다.  그렇지만 첫 번째처럼 예외가 발생하지 않고 interface 타입의 기본값인 nil이 출력되는데 이유는 pointer가 아닌 value 타입으로 생성됐기 때문이다. node1 변수처럼 값형태로 선언만 해도 struct뿐만 아니라 field들도 전부 기본 값들로 초기화한다.

 

마무리로 앞서 말한 예제 코드들로 마치겠다.

 

LinkedList

예제 코드의 구조는

다음과 같고 node 파일부터 시작하겠다.

package linkedlist

type (
	nodeValueType interface {
		string | int64 | any
	}

	node[T nodeValueType] struct {
		prev  *node[T]
		value T
		next  *node[T]
	}
)

func (n *node[T]) setPrev(prev *node[T]) *node[T] {
	n.prev = prev
	return n
}

func (n *node[T]) setValue(value T) *node[T] {
	n.value = value
	return n
}

func (n *node[T]) setNext(next *node[T]) *node[T] {
	n.next = next
	return n
}

private struct를 사용했고 generic을 위해 interface를 사용했다. node struct는 외부로 노출시킬 이유가 없다 판단해서 내부에서만 사용할 수 있게 만들었기 때문에 같은 package를 공유하는 다른 파일에서 접근할 수 있다. setPrev와 setNext가 있는데 method의 parameter로 pointer를 받고 반환은 자기 자신이다. 자기 자신을 반환하면 채널링을 할 수 있어서 좋긴 한데 Golang에서는 뭔가 안 이쁘다. node struct는 데이터를 저장하고 배열과 같이 요소들 간에 순서를 정하기 위해 prev와 next field를 갖고 있다.  value는 Golang의 강타입언어 체제에서 그나마 다른 타입으로 활용할 수 있도록 generic을 이용했다.

 

다음은 파일은 list다.

package linkedlist

import "fmt"

type linkedList[T nodeValueType] struct {
	head *node[T]
	tail *node[T]
}

func (ll *linkedList[T]) PushAll(values ...T) {
	for _, value := range values {
		ll.Push(value)
	}
}

func (ll *linkedList[T]) Push(value T) {
	newNode := new(node[T]).setValue(value)
	pushFn := ll.append
	if ll.IsEmpty() {
		pushFn = ll.setHead
	} else if ll.head != nil && ll.tail == nil {
		pushFn = ll.setTail
	}

	pushFn(newNode)
}

func (ll *linkedList[T]) append(newNode *node[T]) {
	prev := ll.tail
	ll.tail = newNode.setPrev(prev)

	prev.setNext(ll.tail)

}

func (ll *linkedList[T]) setHead(newNode *node[T]) {
	if ll.tail != nil && ll.tail == newNode {
		ll.head = ll.tail.setPrev(nil)
		ll.tail = nil
		return
	}

	n := newNode
	if newNode != nil {
		n = newNode.setPrev(nil)
	}
	ll.head = n
}

func (ll *linkedList[T]) setTail(newNode *node[T]) {
	if newNode != nil && ll.tail == nil {
		ll.head.setNext(newNode)
		newNode.setPrev(ll.head)
	} else if ll.head == newNode {
		ll.head.setNext(nil)
		newNode = nil
	} else if newNode != nil {
		newNode.setNext(nil)
	}

	ll.tail = newNode
}

func (ll *linkedList[T]) Pop() (t T, err error) {
	if ll.IsEmpty() {
		err = fmt.Errorf("linked list empty")
		return
	} else if ll.tail != nil {
		t = ll.tail.value
		ll.setTail(ll.tail.prev)
		return
	}
	t = ll.head.value
	ll.setHead(nil)
	return
}

func (ll *linkedList[T]) Unshift() (t T, err error) {
	if ll.head == nil {
		err = fmt.Errorf("linked list empty")
		return
	}

	t = ll.head.value
	ll.setHead(ll.head.next)

	return
}

func (ll *linkedList[T]) IsEmpty() bool {
	return ll.head == nil && ll.tail == nil
}

이 또한 private struct를 사용했고 Golang에서는 이렇게 많이 사용하는 것 같다. 대신 public method를 지원하는데 Push, PushAll, Pop, Unshift, IsEmpty method를 지원하고 있다. 로직은 LinkedList를 알고 있다면 쉽기 때문에 pass (혹시 궁금하면 댓글 ㄱㄱ)

 

다음은 index 파일

package linkedlist

func New[T nodeValueType](initValues ...T) *linkedList[T] {
	ll := new(linkedList[T])

	for _, value := range initValues {
		ll.Push(value)
	}
	return ll
}

LinkedList를 생성하기 위한 로직만 들어가 있다. 이전 함수 리뷰에서 봤다 ...연산자를 통해 개수 제한 없는 파라미터를 받고 있다.

 

다음은 LinkedList를 사용한 main 파일이다.

package main

import (
	"fmt"
	linkedlist "gh/chap13_14/ex1/ex/datastruct/linkedList"
)

func main() {
	ll := linkedlist.New[string]("hello", " world", "!!!")

	for {
		value, err := ll.Pop()
		if err != nil {
			fmt.Printf("%s\n", err.Error())
			break
		}

		fmt.Printf("Pop %s\n", value)

		if ll.IsEmpty() {
			break
		}
	}

	ll.PushAll("hello", " world", "!!!")

	for {
		value, err := ll.Unshift()
		if err != nil {
			fmt.Printf("%s\n", err.Error())
			break
		}

		fmt.Printf("Unshift %s\n", value)

		if ll.IsEmpty() {
			break
		}
	}

}

단순하게 LinkedList를 사용해 hello world!!!를 저장하는 예제다.

 

아직 이번주 리뷰가 남아있다... ㅃ2

'Golang > Tucker' 카테고리의 다른 글

[묘공단] 4주차  (2) 2023.10.22
[묘공단] 3주차 (2)  (1) 2023.10.16
[묘공단] 2주차 (2)  (1) 2023.10.09
[묘공단] 2주차 (1)  (0) 2023.09.29
[묘공단] 1주차  (0) 2023.09.24