Struct

다양한 struct를 만들며 struct에 대한 학습을 하려함

Backing.go 라는 파일을 만들고, 아래와 같은 struct를 만들어서 main.go에서 Import해서 사용해보려한다. 

package banking

// Account Struct
type Account struct {	
	owner string
	balance int
}

export를 하기 위해 struct의 시작 문자는 대문자로 해주어야 한다.

package main

import (
	"github.com/qazyj/learngo/banking"
)

func main() {
	account := banking.Account{owner:"qazyj", balance: 1000}
}

파일 구조

하지만, field 값을 제대로 못 읽어 오는 듯 하다.

 

이유는 Account가 private이기 때문이다. ?? Java에서 public, private를 선언해주는 것도 아닌데 왜 private인지 의문이 들 것이다. Go에서는 public과 private가 소문자, 대문자로 시작하는 것을 의미한다.

public : 대문자 시작
private : 소문자 시작

 

이처럼 field 변수명도 대문자로 시작해줘야지만 외부 파일에서 사용할 수 있다.

package banking

// Account Struct
type Account struct {	
	Owner string			// 대문자 시작
	Balance int				// 대문자 시작
}

Banking.go

package main

import (
	"github.com/qazyj/learngo/banking"
)

func main() {
	account := banking.Account{Owner:"qazyj", Balance: 1000}
}

Main.go

 

import

대문자 시작으로 바꿔주면 정상적으로 import가 되어 사용이 가능하다. 

 

Java에서는 public, private, protected를 통해 외부 클래스 파일의 접근을 허용한다. 즉, 외부의 접근을 신경쓰기 위해 public, private, protected 중 하나를 추가해야된다는 뜻이다. 하지만, Go에서는 시작 문자를 소/대문자를 통해 public과 private를 구분한다. 코드가 더 간결해지고 한 눈에 알아볼 수 있다는 장점이 있다. 

 

정리

package banking

// Account Struct
type Account struct {	
	owner string			// private
	Balance int				// public
}

 

 

하지만, 나는 Owner라는 이름은 Account를 생성할 때 받고싶지만 Balance는 0이라는 고정값으로 설정하고 싶다. 

func main() {
	account := banking.Account{Owner:"qazyj"}
	fmt.Println(account)
}

출력

Go에서는 struct에 대한 다양한 생성자를 자동으로 만들어주는 듯 하다. struct를 생성할 때 설정되지 않은 필드값은 자동으로 default값으로 설정될 수 있도록 말이다. 위와같이 code를 변경하면, Balance라는 필드값은 private인 balance로 변경해도 문제없이 작동한다.

 

struct의 필드값을 public으로 변경하면 아래와 같은 작업도 가능하다.

func main() {
	account := banking.Account{Owner:"qazyj"}
	account.Owner = "test"
	fmt.Println(account)
}

출력

필드값 변경이 용이하다는 장점이 있지만, 필드값 변경이 무분별하게 진행될 수 있다는 단점도 존재한다. 이러한 단점을 막기위해서는 function을 만들어주어야 한다. 

type Account struct {	
	owner string
	balance int
}

func NewAccount(owner string) *Account{
	account := Account{
		owner: owner,
		balance: 0,
	}
	return &account
}

많은 예제 코드들을 확인해본 결과 생성자에는 New+struct 이름으로 만드는 듯 하다.

func main() {
	account := accounts.NewAccount("qazyj")
	fmt.Println(account)
}

출력

실제 메모리 address를 return하여 복사본이 아닌 만든 값을 return 해주면 된다. account라는 struct를 출력할 때, field값들이 출력되는 이유는 struct를 출력할 때 내부적으로 String 메소드를 호출하여 출력하기 때문이다. String 메소드를 정의하지 않은 경우 Go에서 자동으로 기본 field 값을 포맷팅하여 return 해준다. 링크를 통해 확인해보니 printValue라는 메소드를 통해 아래와 같이 포인트인지 확인한다.

	case reflect.Pointer:
		// pointer to array or slice or struct? ok at top level
		// but not embedded (avoid loops)
		if depth == 0 && f.UnsafePointer() != nil {
			switch a := f.Elem(); a.Kind() {
			case reflect.Array, reflect.Slice, reflect.Struct, reflect.Map:
				p.buf.writeByte('&')
				p.printValue(a, verb, depth+1)
				return
			}
		}
		fallthrough

그 후 다시 printValue로 들어가 아래와 struct인지 확인해주고 출력될 string을 추가로 만들어준다.

	case reflect.Struct:
		if p.fmt.sharpV {
			p.buf.writeString(f.Type().String())
		}
		p.buf.writeByte('{')
		for i := 0; i < f.NumField(); i++ {
			if i > 0 {
				if p.fmt.sharpV {
					p.buf.writeString(commaSpaceString)
				} else {
					p.buf.writeByte(' ')
				}
			}
			if p.fmt.plusV || p.fmt.sharpV {
				if name := f.Type().Field(i).Name; name != "" {
					p.buf.writeString(name)
					p.buf.writeByte(':')
				}
			}
			p.printValue(getField(f, i), verb, depth+1)
		}
		p.buf.writeByte('}')

이러한 코드 방식을 거쳐서 default 값을 만들어준다.

 

내가 원하는 방식으로 string을 출력하고 싶다면 String()이라는 메소드를 만들어주면 된다. override와 비슷한 방식이라고 생각한다. 아래와 같은 메소드를 정의준 뒤, 출력해주자.

// String 메소드 정의
func (a Account) String() string {
	return fmt.Sprintf("Owner: %s, Balance: %d", a.owner, a.balance)
}

출력

이전과는 다른 방식으로 출력이 되는 것을 볼 수 있다. 하지만, field가 추가되는 경우 다시 수정해줘야하는 단점이 존재할 것이다. 기본적으로 제공하는 것을 사용하는 방법이 좋아 보이긴 하다.

 

값을 수정할 수 있는 method를 만들어보려 한다. Go에서 method는 function과는 조금 다르다. 바로 receiver를 추가해줘야 한다. func과 함수명 사이에 receiver를 추가해주면, method가 된다. receiver에서 지켜야 할 사항이 있는데 receiver명은 struct의 첫글자를 따서 사용한다는 것이다. 편의를 위해서 정해놓은 암묵적인 rule 인것 같다. 물론, 지키지않아도 사용가능하다!

// make method
// func receiver name(prameter)
func (a *Account) Deposit(amount int) {
	a.balance += amount
}

위와 같은 method를 이용하여 struct의 값을 private로 설정해도 수정할 수 있다.

 

출력된 balance 값을 확인하기 위해 아래와 같은 method를 추가해주었다.

func (a Account) Balance() int {
	return a.balance
}

확인해본 결과 10이 추가된다.

tip)
(a *Account) 에서 *를 붙여준 이유는 *를 붙여줘야지만 내가 설정해놓은 struct를 receiver로 가져오기 때문이다. *를 제외하면 struct를 복사해서 가져오기때문에 수정한 값이 적용되지 않는다.

 

 

 

Error handling

Go에서는 exception, try-catch와 같은 에러 핸들링 도구가 존재하지 않는다. 그렇기때문에 직접 error를 직접 체크하고 return 해줘야 한다. 아래와 같은 기능을 하는 Withdraw 메소드를 만든다.

func(a *Account) Withdraw(amount int) {
	a.balance -= amount
}

이때, a.balance 값이 amount 값보다 작은 경우 작업을 하지않고 error를 return 해주려고 한다.

func(a *Account) Withdraw(amount int) error {
	if a.balance < amount {
		return errors.New("Can`t withdraw")
	}
	a.balance -= amount
	return nil
}

위와같이 작성해주면 된다. Go에서 error는 error와 nil이라는 값으로 나타낼 수 있다. 보통 nil은 에러가 없다는 값으로 사용한다. main에서 한 번 확인해보자.

func main() {
	account := accounts.NewAccount("qazyj")
	account.Deposit(10)
	fmt.Println(account.Balance())
	err := account.Withdraw(20)
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(account.Balance())
}

출력

balance값보다 큰 값으로 Withdraw를 했기때문에 nil이 아닌 error가 return이 되었고, if문을 통해 err가 nil이 아닌 경우 출력하여 제대로 error handling을 하는지 확인했다.

Go에서는 error가 발생했다고 시스템을 종료시키거나 하지 않는다. 순전히 개발자가 error를 스스로 return해주고, error에 관한 처리를 스스로 해주어야만 한다. 그렇기때문에 error를 return해도 아래 코드들이 정상적으로  실행된다.

 

에러를 로그로 남기며 시스템을 종료하고 싶다면, 아래와 같이 handling할 수 있다.

func main() {
	account := accounts.NewAccount("qazyj")
	account.Deposit(10)
	fmt.Println(account.Balance())
	err := account.Withdraw(20)
	if err != nil {
		log.Fatalln(err)
	}
	fmt.Println(account.Balance())
}

출력

error가 return되었고, error가 return되었을 때 log를 찍으며 치명적인 로그이기때문에 시스템을 종료하도록 하는 것이다. 시스템이 정상적으로 돌아갔다면 10이 한번 더 출력이 되어야 하지만, 바로 종료되었기 때문에 출력이되지 않는 모습이다. 

'Go' 카테고리의 다른 글

URL Checker & Go Routine  (0) 2024.01.14
method를 활용한 map 구현(Search, Add, Delete, Update)  (0) 2024.01.10
Struct  (0) 2024.01.04
Maps  (0) 2023.12.30
Arrays/Slices  (0) 2023.12.29

+ Recent posts