본문 바로가기

Go

[묘공단] Tucker의 Go 언어 프로그래밍 22장~23장

  • 22장 자료구조
  • 23장 에러핸들링

 

 

22장 자료구조

 

 

 

  • 리스트의 기본적인 사용법 
  • list.New()로 새로운 인스턴스를 만들어 PushBack, PushFront, InsertBefore, InsertAfter함수를 사용하여, 리스트의 여러위치에 요소를 삽입할수있다. 
  • Front(),Next()함수를 사용하여 각 요소별 순회가 가능하다. e는 List의 pointer변수이다.
//ch22/ex22.1/ex22.1.go
package main

import (
	"container/list"
	"fmt"
)

func main() {
	v := list.New()       // ❶ 새로운 리스트 생성
	e4 := v.PushBack(4)   // ❷ 리스트 뒤에 요소 추가
	e1 := v.PushFront(1)  // ❸ 리스트 앞에 요소 추가
	v.InsertBefore(3, e4) // ❹ e4 요소 앞에 요소 삽입
	v.InsertAfter(2, e1)  // ➎ e1 요소 뒤에 요소 삽입

	for e := v.Front(); e != nil; e = e.Next() { // ➏ 각 요소 순회
		fmt.Print(e.Value, " ")
	}

	fmt.Println()
	for e := v.Back(); e != nil; e = e.Prev() { // ➐ 각 요소 역순 순회
		fmt.Print(e.Value, " ")
	}
}

 

 

  • 아래는 배열,슬라이스와 리스트의 Big-O 표기법이다.

 

 

 

 

 

  • 리스트를 사용하여 Queue를 구현할수있다. 
//ch22/ex22.2/ex22.2.go
package main

import (
	"container/list"
	"fmt"
)

type Queue struct { // ❶ Queue 구조체 정의
	v *list.List
}

func (q *Queue) Push(val interface{}) { // ❷ 요소 추가
	q.v.PushBack(val)
}

func (q *Queue) Pop() interface{} { // ❸ 요소을 반환하면서 삭제
	front := q.v.Front()
	if front != nil {
		return q.v.Remove(front)
	}
	return nil
}

func NewQueue() *Queue {
	return &Queue{list.New()}
}

func main() {
	queue := NewQueue() // ❹ 새로운 큐 생성

	for i := 1; i < 5; i++ { // ➎ 요소 입력
		queue.Push(i)
	}
	v := queue.Pop()
	for v != nil { // ➏ 요소 출력
		fmt.Printf("%v -> ", v)
		v = queue.Pop()
	}
}

 

 

  • 아래소스는 리스트를 사용하여 스택을 만드는 예제이다.
//ch22/ex22.3/ex22.3.go
package main

import (
	"container/list"
	"fmt"
)

type Stack struct {
	v *list.List
}

func NewStack() *Stack {
	return &Stack{list.New()}
}

func (s *Stack) Push(val interface{}) {
	s.v.PushBack(val) // ❶ 맨 뒤에 요소 추가
}

func (s *Stack) Pop() interface{} {
	back := s.v.Back() // ❷ 맨 뒤에서 요소를 반환
	if back != nil {
		return s.v.Remove(back)
	}
	return nil
}

func main() {
	stack := NewStack()

	for i := 1; i < 5; i++ {
		stack.Push(i)
	}

	val := stack.Pop()
	for val != nil {
		fmt.Printf("%v -> ", val)
		val = stack.Pop()
	}
}

 

 

  • 아래의 소스를 리스트를 사용하여 링 자료구조를 구현하였다. 
//ch22/ex22.4/ex22.4.go
package main

import (
	"container/ring"
	"fmt"
)

func main() {
	r := ring.New(5) // ❶ 요소가 5개인 링 생성

	n := r.Len() // ❷ 링 개수 반환

	for i := 0; i < n; i++ {
		r.Value = 'A' + i // ❸ 순회하면 모든 요소에 값 대입
		r = r.Next()
	}

	for j := 0; j < n; j++ {
		fmt.Printf("%c ", r.Value) // ❹ 순회하며 값 출력
		r = r.Next()
	}

	fmt.Println() // 한줄 띄우기

	for j := 0; j < n; j++ {
		fmt.Printf("%c ", r.Value) // ➎ 순회하며 값 출력
		r = r.Prev()
	}
}

 

 

 

  • 아래 소스는 맵을 사용한 기본예제인데,
  • 맵을 생성하고, 값을 추가하거나, 변경하고 출력하는 예제이다.
//ch22/ex22.5/ex22.5.go
package main

import "fmt"

func main() {
	m := make(map[string]string) // ❶ 맵 생성
	m["이화랑"] = "서울시 광진구"
	m["송하나"] = "서울시 강남구"
	m["백두산"] = "부산시 사하구"
	m["최번개"] = "전주시 덕진구"

	m["최번개"] = "청주시 상당구" // ❸ 값 변경

	fmt.Printf("송하나의 주소는 %s입니다.\n", m["송하나"])
	fmt.Printf("백두산의 주소는 %s입니다.\n", m["백두산"])
}

 

 

  • 아래 예제는 다른 용도로 사용되는 맵의 소스이다.
  • key값은 int로 , value는 Product구조체이다.
//ch22/ex22.6/ex22.6.go
package main

import "fmt"

type Product struct {
	Name  string
	Price int
}

func main() {
	m := make(map[int]Product) // ❶ 맵 생성

	m[16] = Product{"볼펜", 500}
	m[46] = Product{"지우개", 200}
	m[78] = Product{"자", 1000}
	m[345] = Product{"샤프", 3000}
	m[897] = Product{"샤프심", 500}

	for k, v := range m { // ❸ 맵 순회
		fmt.Println(k, v)
	}
}

 

 

 

 

  • 맵에서 아래 소스와 같이 값을 삭제할수도있다.
//ch22/ex22.7/ex22.7.go
package main

import "fmt"

func main() {
	m := make(map[int]int) // ❶ 맵 생성
	m[1] = 0               // ❷ 요소 추가
	m[2] = 2
	m[3] = 3

	delete(m, 3)      // ❸ 요소 삭제
	delete(m, 4)      // ❹ 없는 요소 삭제 시도
	fmt.Println(m[3]) // ➎ 삭제된 요소 값 출력
	fmt.Println(m[1]) // ➏ 존재하는 요소 값 출력
}

 

 

  • 아래는 간단한 해시함수를 사용하여 맵을 만드는 예제이다.
  • 만약에 m[hash(33)] = 50 으로 값을 저장한다면 hash(23)과 충돌한다. 이런경우 가장 단순한 해결방법은 인덱스 위치마다 값이 아니라 리스트를 저장하게 하면 해결할수있다. 
//ch22/ex22.8/ex22.8.go
package main

import "fmt"

const M = 10 // ❶ 나머지 연산의 분모

func hash(d int) int {
	return d % M // ❷ 나머지 연산
}

func main() {
	m := [M]int{} // ❸ 값을 저장할 배열 생성

	m[hash(23)] = 10  // ❹ 키 23에 값 설정
	m[hash(259)] = 50 // ➎ 키 259에 값 설정

	fmt.Printf("%d = %d\n", 23, m[hash(23)])
	fmt.Printf("%d = %d\n", 259, m[hash(259)])
}

 

 

23장 에러핸들링

 

  • WriteFile()함수는 파일에 string을 저장하고, ReadFile()함수는 data.txt파일의 내용을 읽어서 반환하는 함수이다.
//ch23/ex23.1/ex23.1.go
package main

import (
	"bufio"
	"fmt"
	"os"
)

func ReadFile(filename string) (string, error) {
	file, err := os.Open(filename) // ❶ 파일 열기
	if err != nil {
		return "", err // ❷ 에러 나면 에러 반환
	}
	defer file.Close()          // ❸ 함수 종료 직전 파일 닫기
	rd := bufio.NewReader(file) // ❹ 파일 내용 읽기
	line, _ := rd.ReadString('\n')
	return line, nil
}

func WriteFile(filename string, line string) error {
	file, err := os.Create(filename) // ➎ 파일 생성
	if err != nil {                  // ➏ 에러 나면 에러 반환
		return err
	}
	defer file.Close()
	_, err = fmt.Fprintln(file, line) // ➐ 파일에 문자열 쓰기
	return err
}

const filename string = "data.txt"

func main() {
	line, err := ReadFile(filename) // ➑ 파일 읽기 시도
	if err != nil {
		err = WriteFile(filename, "This is WriteFile") // ➒ 파일 생성
		if err != nil {                                // ❿ 에러를 처리
			fmt.Println("파일 생성에 실패했습니다.", err)
			return
		}
		line, err = ReadFile(filename) // ⓫ 다시 읽기 시도
		if err != nil {
			fmt.Println("파일 읽기에 실패했습니다.", err)
			return
		}
	}
	fmt.Println("파일내용:", line) // ⓬ 파일 내용 출력
}

 

 

  • 위의 소스는 함수에서 발생한 에러를 처리하였고, 아래의 소스는 직접 에러를 만들어서 반환하는 함수를 만들어보겠다.
  • Sqrt함수는 인자로 들어온값이 음수이면 에러를 반환하도록 하였다.
//ch23/ex23.2/ex23.2.go
package main

import (
	"fmt"
	"math"
)

func Sqrt(f float64) (float64, error) {
	if f < 0 {
		return 0, fmt.Errorf(
			"제곱근은 양수여야 합니다. f:%g", f) // ❶ f가 음수이면 에러 반환
	}
	return math.Sqrt(f), nil
}

func main() {
	sqrt, err := Sqrt(-2)
	if err != nil {
		fmt.Printf("Error: %v\n", err) // ❷ 에러 출력
		return
	}
	fmt.Printf("Sqrt(-2) = %v\n", sqrt)
}

 

 

 

  • 아래소스를 보면 PasswordError구조체를 선언하고, Error()함수를 해당 구조체의 메서드로 선언하였다.
  • RegisterAccount함수에서 에러가 발생하면 PasswordError객체를 반환하고, main함수에서 반환된객체가 null이 아니라면 에러가 반환된다. 해당 에러객체를 출력하게 된다.
//ch23/ex23.3/ex23.3.go
package main

import "fmt"

type PasswordError struct { // ❶ 에러 구조체 선언
	Len        int
	RequireLen int
}

// PasswordError구조체에 대해서 Error() 메서드가 구현되어있다.
func (err PasswordError) Error() string { // ❷ Error() 메서드
	return "암호 길이가 짧습니다."
}

// error인터페이스를 리턴해준다. PasswordError구조체는 Error()메서드를 가지고 있기때문에 문제없음.
func RegisterAccount(name, password string) error {
	if len(password) < 8 {
		return PasswordError{len(password), 8} // ❸ error 반환
	}

	return nil
}

func main() {
	err := RegisterAccount("myID", "myPw") // ➍ ID, PW 입력
	if err != nil {                        // ➎ 에러 확인
		if errInfo, ok := err.(PasswordError); ok { // ➏ 인터페이스 변환
			fmt.Printf("%v Len:%d RequireLen:%d\n",
				errInfo, errInfo.Len, errInfo.RequireLen)
		}
	} else {
		fmt.Println("회원 가입되었습니다.")
	}
}

 

 

  • 아래소스는 문자열에서 두단어를 읽어서 숫자로 변환후 곱한결과를 반환하는 소스이다.
//ch23/ex23.4/ex23.4.go
package main

import (
	"bufio"
	"errors"
	"fmt"
	"strconv"
	"strings"
)

func MultipleFromString(str string) (int, error) {
	scanner := bufio.NewScanner(strings.NewReader(str)) // ❶ 스캐너 생성
	scanner.Split(bufio.ScanWords)                      // ❷ 한 단어씩 끊어읽기

	pos := 0
	a, n, err := readNextInt(scanner)
	if err != nil {
		return 0, fmt.Errorf("Failed to readNextInt(), pos:%d err:%w", pos, err) // ➏ 에러 감싸기
	}

	pos += n + 1
	b, n, err := readNextInt(scanner)
	if err != nil {
		return 0, fmt.Errorf("Failed to readNextInt(), pos:%d err:%w", pos, err)
	}
	return a * b, nil
}

// 다음 단어를 읽어서 숫자로 변환하여 반환합니다.
// 변환된 숫자, 읽은 글자수, 에러를 반환합니다.
func readNextInt(scanner *bufio.Scanner) (int, int, error) {
	if !scanner.Scan() { // ❸ 단어 읽기
		return 0, 0, fmt.Errorf("Failed to scan")
	}
	word := scanner.Text()
	number, err := strconv.Atoi(word) // ❹ 문자열을 숫자로 변환
	if err != nil {
		return 0, 0, fmt.Errorf("Failed to convert word to int, word:%s err:%w", word, err) // ➎ 에러 감싸기
	}
	return number, len(word), nil
}

func readEq(eq string) {
	rst, err := MultipleFromString(eq)
	if err == nil {
		fmt.Println(rst)
	} else {
		fmt.Println(err)
		var numError *strconv.NumError
		if errors.As(err, &numError) { // ➐ 감싸진 에러가 NumError인지 확인
			fmt.Println("NumberError:", numError)
		}
	}
}

func main() {
	readEq("123 3")
	readEq("123 abc")
}

 

 

 

  • 패닉은 프로그램을 정상 진행시키기 어려운 상황을 만났을때 프로그램 흐름을 중지시키는 기능이다. Go에서는 panic()으로 내장함수를 제공한다.
//ch23/ex23.5/ex23.5.go
package main

import "fmt"

func divide(a, b int) {
	if b == 0 {
		panic("b는 0일 수 없습니다") // ❶ Panic 발생
	}
	fmt.Printf("%d / %d = %d\n", a, b, a/b)
}

func main() {
	divide(9, 3)
	divide(9, 0) // ❷ Panic 발생
}

 

 

 

 

  • 프로그램이 문제가 발생할때 프로그램이 종료되는 대신 에러메세지를 표시하고 복구를 시도하는게 낫다. 
  • 아래 소스는 패닉이 발생하였을때 복구를 하는 소스이다.
  • recover()함수에서 패닉이 전파중이면 panic객체를 출력하고, 계속 진행한다.
//ch23/ex23.6/ex23.6.go
package main

import "fmt"

func f() {
	fmt.Println("f() 함수 시작")
	defer func() { // ❹ 패닉 복구
		if r := recover(); r != nil {
			fmt.Println("panic 복구 -", r)
		}
	}()

	g() // ❶ g() -> h() 순서로 호출
	fmt.Println("f() 함수 끝")
}

func g() {
	fmt.Printf("9 / 3 = %d\n", h(9, 3))
	fmt.Printf("9 / 0 = %d\n", h(9, 0)) // ❷ h() 함수 호출 - 패닉
}

func h(a, b int) int {
	if b == 0 {
		panic("제수는 0일 수 없습니다.") // ❸ 패닉 발생!!
	}
	return a / b
}

func main() {
	f()
	fmt.Println("프로그램이 계속 실행됨") // ➎ 프로그램 실행 지속됨
}