Go 언어 배워보기 - 기초문법 4

2023.02.14

Map

Map은 키:값 으로 이루어진 자료 구조이다. go에서 map을 사용하려면 map[key 타입]value 타입 으로 선언한다. 슬라이스와 마찬가지로 선언할때 초기화를 하거나 make() 함수로 생성이 가능하다

list := make(map[string]string)    //키 타입이 string이고 값도 string 타입인 map 생성
list2 := map[int]string{        //키 타입이 int 형이고 값이 string인 map을 선언과 동시에 초기화
    1:"값1",
    2:"값2",
    3:"값3"
}

Map에 값 추가하기

map[키]=값 으로 Map에 값을 추가할수있다. 이때 이미 할당된 key값 이라면 새로운 값으로 덮어씌운다.

list := make(map[string]string)

list["first"]="first value"
list["second"]="second value"
list["first"]="third value"    //first 키값은 이미 존재하므로 third value 라는 값이로 덮어씌운다.

Map 값 체크

map의 값을 확인할때 실제로 2개의 값을 리턴하는데 두번째로 오는 exists 로 해당 key의 값이 존재하는지 확인할수 있다.

val,exsists := map[2]    //2번 키에 대한 값이 없다면 exsists는 false가 된다.



메소드

go 에서는 클래스(class)라는 것 자체가 없다고 하더라. 객체지향 프로그래밍에서 class가 데이터와 기능을 둘다 가지는것과 달리 go에서 구조체는 오직 필드만을 가지고 기능은 메소드라고 불리는 함수로 별개로 구현한다고 한다.
go 에서 메소드란 리시버(receiver) 라고 불리는 특별한 인자를 가진 함수이다. 이 리시버는 func 키워드와 함수명 사이에 정의한다. 이 리시버는 해당 메소드가 속하는 구조체의 타입과 변수명을 지정하고 함수 안에서 사용할수 있다.

type User struct {
	name string
	age  int
}

//User 구조체에 getUser() 라는 메소드를 정의했다.
func (u User) getUser() (string, int) {
	return u.name, u.age
}

func main() {
    p := User{"이름",2345}
    //위에서 정의한 메소드는 .getUser()로 호출가능
    fmt.Println(p.getUser())    //이름 2345 출력
}


데이터 타입에 메소드 선언

또한 구조체 뿐만 아니라 어떤 타입 이든 메소드를 작성할수있다. 단 int 나 float64 같은 원시 타입에 직접 선언 하는건 불가능하고 같은 패키지 안에서 선언된 타입에 대해서만 메소드를 선언할수있다.

type newInt int    //newInt 라는 새로운 타입을 정의

func (i newInt) double() int {        //newInt type에 메소드 정의
    /*
        여기서 int로 타입변환을 한 이유는 i 라는 변수를 'newInt' 라는 새로운 타입으로 인식하기때문에
        int 형으로 바꿔준다
    */
    return int(i) * 2    
}

func main() {
    num := newInt(10)
    res := num.double()
    print(res)
}


value 리시버와 포인터 리시버

메소드의 리시버도 다른 변수와 마찬가지로 또는 포인터를 넘겨줄수있다. 리시버에 값을 넘기면 메소드 내부에서는 복사된 값을 사용하고 포인터를 넘긴다면 실제 포인터 주소의 값이 변경되어 메소드 호출후 포인터 원본도 바뀌게 된다.

type User struct {
	name string
	age  int
}

func (u User) changeAge(value int) {       //이 메소드는 구조체의 값을 바꾸지 않는다.
	u.age = value
}

func (u *User) changeAgePointer(value int) {    //포인터 리시버를 가진 메소드는 실제 값을 수정한다.
	u.age = value
}

func main() {
	user := User{"name", 18}
	fmt.Println(user.age)    //18
	user.changeAge(25)
	fmt.Println(user.age)    //18
	user.changeAgePointer(35)
	fmt.Println(user.age)    //35
}

인터페이스

인터페이스란 타입에 구현해야하는 메소드들을 모아두는 집합이다. type 인터페이스명 interface 로 선언하고 내부에는 구현해야할 메소드 명 과 타입을 정의한다.

type MyInterface interface {
    getAge() int        //메소드명, 리턴 타입을 정의
    getName() string
    setAge(int)    //파라미터 타입 정의
}

그리고 이 인터페이스를 구현하는 메소드들을 작성한다.

type MyStruct struct {
    name string
    age int
}

//인터페이스에 정의된 getAge(),getName(),setAge() 메소드들을 구현한다.
func (s MyStruct) getName() string {
    return s.name
}

func (s MyStruct) getAge() int {
    return s.age
}

func (s *MyStruct) setAge(age int) {
    s.age=age
}

MyStruct 구조체와 MyInterface 인터페이스로 변수를 선언하고 MyStruct 변수를 MyInterface 변수에 할당한다.

s := MyStruct{"이름",123}
i := MyInterface

i.getName()    //s 변수에 있던 메소드를 사용한다.

기본값이 nil 인 인터페이스

인터페이스를 구현한 구조체의 값이 nil일때(인터페이스를 구현한 구조체값을 선언 안했을때) 메소드를 호출하면 다른 언어의경우 null pointer exception을 발생시키지만 go에서는 직접 예외처를 해줘야한다.

import "fmt"

import "fmt"

type text struct {
	text string
}

type textInterface interface {
	print()
}

//text 구조체에 인터페이스 메소드 구현
func (t *text) print() {
	if t == nil {        //t 가 nil일 경우 "nil" 출력하고 리턴
		fmt.Println("nil")
		return
	}
	fmt.Println(t.text)
}

func main() {
	var i textInterface    //textInterface 인터페이스 변수 선언
	var t *text            //text 구조체 타입의 변수 선언
	i = t                //구조체를 인터페이스 할당
	i.print()            //print() 메소드를 호출
}

main 안에서 text 구조체 타입 변수를 선언했지만 값을 할당하지 않아 nil 값이다. 이걸 textInterface 타입의 변수에 다시 할당하고 print() 메소드를 호출하는데 이때 print() 메소드의 리시버 값이 nil 이기때문에 print() 메소드에 if문으로 nil을 체크하지 않는다면 에러가 발생한다.


빈(empty) 인터페이스

인터페이스 내부에 아무 메소드도 선언하지 않은 걸 빈 인터페이스라고 한다.

i := interface{}

빈 인터페이스에는 어떤 값이든 넣을수있다.

i = 10
i = "String"

빈 인터페이스는 알수없는 값을 처리할때 사용된다. 예를들어 go의 fmt.Print 함수는 인자갑이 빈 인터페이스로 되어있다.


인터페이스의 타입 선언 (type assertion)

빈 인터페이스 변수에 명시적으로 타입을 가져야 한다고 선언할수있다. 빈 인터페이스에 대해 설명했듯이 어떤값이든 가질수있지만 특정 타입의 값을 가지고 있는걸 확인하는것이다.

value := i.(int)

이 문법은 int 타입을 가진 인터페이스 변수value라는 변수에 가지고있는 값을 할당한다는 뜻이다. 만약 i의 값이 nil 이거나 int형이 아닐경우 panic 에러가 발생한다.
타입 선언은 두개의 값을 리턴하는데 첫번째는 인터페이스의 값이며 두번째는 타입 선언의 성공여부를 리턴한다. 이때 성공여부까지 같이 반환 받는다면 panic 에러가 발생하지 않는다.

var i interface{} = "string"

a := i.(string) 
fmt.Println(a)    //"string"
a, ok := i.(string) 
fmt.Println(a, ok)    //성공여부까지 확인, "string",true
b, ok := i.(int) 
fmt.Println(b, ok)    //i의 값은 string 타입, 이때 t의 값은 해당타입의 기본값(zero value)가 되며 ok는 false가 된다.
c := i.(int) //성공여부를 받지 않으면 panic 에러가 발생한다.
fmt.Println(c)

Do you want something exciting?

© 2022. YSH All rights reserved.