java, kotlin 개발자가 공부해보는 go (문법)

박우영·2024년 11월 27일
0

go

목록 보기
1/1

Overview

go 입문이지만 Java, Kotlin 문법과 다른 것, 생소한것 위주로 작성 하였습니다. 따라서 상세하게 작성하기보단 개인필기 하듯 작성 하였습니다.

자료구조

List, Queue, Stack 와 같은 Java 에서도 지원하는 자료구조는 제외

Ring 은 맨 뒤의 요소와 맨 앞의 요소가 서로 연결된 자료구조 이다. 리스트를 기반으로 만들어진 자료구조로, 원형으로 연결되어 있기 때문에 환형 리스트 라고도 부른다.



Ring 을 순회하며 Print 하는 함수

func main() {
	r := ring.New(5)
	n := r.Len()

	for i := 0; i < n; i++ {
		r.Value = 'A' + i
		r = r.Next()
	}

	for i := 0; i < n; i++ {
		fmt.Printf("%c", r.Value)
		r = r.Next()
	}

	fmt.Println()

	for i := 0; i < n; i++ {
		fmt.Printf("%c", r.Value)
		r = r.Prev()
	}
}
ABCDE
AEDCB

r 의 값은 r.Next(), r.Prev() 와 같이 현재 위치를 갱신하며 해당 Value 를 출력한다.

Next 에서 마지막 요소가 출력되고 Next()가 호출되어 다시 처음 위치로 돌아간 것이다.

언제쓸까?

링은 저장할 개수가 고정되고, 오래된 요소는 지워도 되는 경우 적합하다. 예를 들어 MS 워드 는 ctrl + Z

를 눌러 실행을 취소할 수 있는 것 처럼 이 기능을 지원하려면 지금까지 쓴 내용을 보관하고 있어야 한다.

  1. 실행취소: 문서 편집기 등에서 일정한 개수의 명령을 저장하고 실행 취소하는 경우
  2. 고정 크기 버퍼 기능: 데이터에 따라 버퍼가 증가되지 않고 고정된 길이로 쓸 때
  3. 리플레이 기능: 게임 등에서 최근 플레이 10초를 다시 리플레이할 때와 같이 고정된 길이의 리플레이 기능을 제공할 때

interface

go 에서 Interface 를 구현하는 방법은 다음과 같다

func main() {
	var stringer = &Stringer{}
	var wirter Wirter = stringer
	fmt.Print(wirter.Wirte())
	fmt.Print(stringer.Wirte())
}

type Reader interface {
	Read() string
	Close() error
}

type Wirter interface {
	Wirte() string
	Close() error
}

type ReadWirter interface {
	Reader
	Wirter
}

type Stringer struct {
}

func (s *Stringer) Wirte() string {
	return "write"
}

func (s *Stringer) Close() error {
	return nil
}

Java 처럼 Implements 를 하지않고 함수 하나하나 만들어 구현을 한다.

개인적으론 Java 가 나은듯 그래도 함수형 프로그래밍 스럽다고 느꼈고 컴파일에러 를 발생시켜 문제는 없을 것 같다.

go 는 Type casting 결과 값으로 결과와 성공 여부를 반환한다.

var a Interface
t, ok := a.(ConcreteType)

t: 타입 변환한 결과
ok: 변환 성공여부

👍 best practice

fun ReadFile(reader Reader) {
	c, ok := reader.(Closer)
	if ok {
		c.Close()
	}
}

or
if c, ok := reader.(Closer); ok {
...
}

다음과 같이 Type casting 을 한다면 반환값으로 주어진 bool 값으로 확인해주는게 RunTime 환경에서 에러를 방지 할 수 있다 (Java 의 instance of 와 같은 개념)

Error Handling

에러 반환

에러를 처리하는 가장 기본 방식은 에러를 반환하고 알맞게 처리하는 방식이다. 예를 들어 ReadFile() 함수로 파일을 읽을 때 해당하는 파일이 없어 에러가 발생했다고 하자. 이럴 때 프로그램이 강제 종료 되는 것보다는 적절한 메시지를 출력하고 다른 파일을 읽거나 임시 파일을 생성하는 것이 사용자 경험에 더 좋을 것이다.

func main() {
	line, err := ReadFile("someFileName")
	if err != nil {
		// error 처리
	}
	println(line)
}

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
}

Java 에서 Exception 을 throw 한다면 go 는 error 를 return 한다.

Error 생성 방법

fmt.Errorf("Error 를 반환 합니다")
errors.New("에러 메시지")

error 는 어떤 타입일까

type error interface {
	Error() string
}

다음과 같이 문자열을 반환하는 Error() 메소드로 구성되어있다. 따라서 Error() 를 구현하는 CustomError 도 구현 가능하다는 말인데

type PasswordError struct {
	Len int
	RequireLen int
}

func (err PasswordError) Error() string {
	return "암호 길이가 짧습니다"
}

func RegisterAccount(name, password string) error {
	if len(password) < 8 {
		return PasswordError { len(password), 8 }
	}
}

다음과 같이 회원을 등록할때 customError 로 password validation 을 구현할 수 있을것이다.

패닉(panic)

패닉은 프로그램을 정상 진행시키기 어려운 상황을 만났을 때 프로그램 흐름을 중지시키는 기능이다.

지금까지 알아본 error 인터페이스는 호출자에게 문제원인을 알려주기 위해 사용 했다면, 패닉은 좀 더 크리티컬한 문제가 발생하여 프로그램을 종료시켜 문제를 빠르게 인지하고 싶을때 사용한다.

(예시로 메모리 부족, 예상치 못한 크리티컬한 버그 등이 있다.)

panic 이 호출된다면 java 에서 exception 이 발생했을때 나오는 stackTrace 와 유사한 call stack 을 호출한다.

Stack Trace와 Call Stack의 차이

  • Stack Trace는 주로 오류 발생 시점의 호출 경로를 기록한 것으로, 디버깅과 오류 분석을 위해 사용됩니다.
  • Call Stack프로그램의 현재 실행 상태를 유지하는 구조로, 실행 중인 함수 호출을 관리합니다.

panic 의 사용법 또한 간단하다.

func main() {
	isPanic()
	fmt.Print("패닉 발생 후")
}

func isPanic() {
	panic("패닉 발생~")
}

call stack

panic: 패닉 발생~

goroutine 1 [running]:
main.isPanic(...)
        /Users/park/Desktop/go/hello/hello.go:13
main.main()
        /Users/park/Desktop/go/hello/hello.go:8 +0x30

하지만 버그가 발생했다고 알림도없이 종료만 된다면 사용자 경험을 해칠 수 있기 때문에 패닉의 전파와 복구가 중요하다.

func main() {
	recovery()
}

func recovery() {
	defer func() {
		if r := recover(); r != nil {
			fmt.Print("panic 복구 - ", r)
		}
	}()

	facade()
	fmt.Print("함수 끝")
}

func facade() {
	hasPanic()
}

func hasPanic() {
	panic("패닉 발생")
}

위 예제와 같이 defer를 통해 전파된 panic 을 복구하는 함수를 짤 수 있다.

활용 해보기

빈 Interface 활용

fmt 의 Print 함수는 가변 인수를 처리할 수 있다.

func Print(args ...interface{}) string {
	for _, arg := range args {
		switch f := arg.(type) {
			case bool:
				val := arg.(bool)
				...
		}
	}
}

빈 interface 를 제너릭 하게 사용하는 듯 하다.

derfer 지연 실행

함수가 종료되기 직전에 실행해야 하는 코드가 있을 수 있다. 대표적으로 파일이나 소캣 핸들처럼 OS 내부 자원을 사용하는 경우 반환하지 않는다면 OS 의 내부자원을 사용할 수 없기 때문이다.

그래서 이처럼 함수 종료 전에 처리 해야 하는 코드가 있을때 defer 를 사용한다.

import (
	"fmt"
	"os"
)

func main() {
	f, err := os.Create("test.txt")
	if err != nil {
		fmt.Println("Failed to create a file")
		return
	}

	defer fmt.Println("반드시 호출된다")
	defer f.Close()
	defer fmt.Println("파일을 닫았습니다")

	fmt.Println("파일에 Hello world 를 쓴다.")
	fmt.Fprintln(f, "Hello world")
}

출력 결과

파일에 Hello world 를 쓴다.
파일을 닫았습니다
반드시 호출된다

출력 결과를 보면 알 수 있듯이 defer 함수는 역순으로 호출된다. 이 점 유의하자

함수타입 변수

Hardware level 에서 제어하는 Program Counter 라는 개념이 있는,. 이는 명령을 실행하며 다음 실행 해야 하는 명령어를 찾을때 사용된다. go 는 함수도 숫자로 표현 할 수 있다. 따라서 함수 포인터를 활용할 수 있다는 의미다.

함수 타입을 나타내는것은 다음과 같이 표현 한다.

func (int, int) intint Type 일 필요는 없다. input 과 output 에 대한 원하는 type 을 명시하면 된다.

example

import (
	"fmt"
)

func main() {
	var oper func(int, int) int
	oper = getOperator("*")
	var result = oper(3, 4)
	fmt.Print(result)
}

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

func mul(a, b int) int {
	return a * b
}

func getOperator(op string) func(int, int) int {
	if op == "+" {
		return add
	} else if op == "*" {
		return mul
	}
	return nil
}

getOperator 함수는 int type 의 파라미터를 2개 받고 int 를 return 하는 함수를 호출 한다.

함수 리터럴

흔히 말하는 익명함수 lamda 라고 불리는걸 go 에선 함수 리터럴 이라고 부른다.

import (
	"fmt"
)

func main() {
	oper := getOperator("*")
	var result = oper(4, 4)
	fmt.Print(result)
}

type opFunc func(a, b int) int

func getOperator(op string) opFunc {
	if op == "+" {
		return func(i1, i2 int) int {
			return i1 + i2
		}
	} else if op == "*" {
		return func(a, b int) int {
			return a * b
		}
	}
	return nil
}

Java 에서의 Funtional Interface 를 정의하는 것과 유사하다.

주의 사항으로는 리터럴 오부 변수를 내부 상태로 가져올땐 capture 하는데, capture 는 값 복사가 아니라 참조 형태로 가져오게 된다.

Next Step

사실 go 하면 goroutine 이 나와야 하지만 한 게시글에 담기엔 너무 많다고 판단하였습니다. 다음은 go 의 goroutine 에 대해 알아보겠습니다.

0개의 댓글