Go

Go의 복합 타입과 함수

구루싸 2021. 12. 16. 09:57
반응형
SMALL

배열

배열은 0개 이상의 특정 타입 원소로 이뤄진 고정 길이 시퀀스다. 배열의 개별 원소는 기존 첨자 표기법으로 접근하며, 첨자의 범위는 0부터 배열 길이-1까지다. 내장 함수 len은 배열의 원소 수를 반환한다.

var a [3]int 			// 정수 3개로 이뤄진 배열
fmt.Println(a[0])		// 첫 번째 원소 출력
fmt.Println(a[len(a)-1])// 마지막 원소 출력
// 인덱스와 원소 출력
for i, v := range a {
	fmt.Printf("%d %d\n", i, v)
}
// 원소만 출력
for _, v := range a {
	fmt.Printf("%d\n", v)
}
var q [3]int = [3]int{1, 2, 3}
var r [3]int = [3]int{1, 2} 
fmt.Println(r[2]) // 0

p := [...]int{1, 2, 3}
fmt.Printf("%T\n", q) // [3]int

func zero(ptr *[32]byte) {
	for i := range ptr {
    	ptr[i] = 0
    }
}

배열에 대한 포인터를 사용하는 것은 효율적이고 호출된 함수가 호출자의 변수를 변경할 수 있게 하지만 배열은 고정 크기라는 점 때문에 본질적으로 유연하지 못하다. 그렇기 때문에 SHA256 고정 크기 해시와 같은 특별한 경우 외에는 배열을 거의 함수 인자로 사용하지 않고 대신 슬라이스를 사용한다.

슬라이스

슬라이스는 모든 원소가 같은 타입인 가변 길이 시퀀스를 나타낸다. 슬라이스 타입은 원소가 T 타입일 때 []T로 쓴다. 이는 크기가 없는 배열 타입처럼 보인다. 배열과 슬라이스는 밀접하게 연결돼 있다. 슬라이스는 '슬라이스의 내부 배열' 이라고 알려진 배열의 원소들 일부 또는 전부에 접근할 수 있는 경량 자료 구조다. 슬라이스에는 세 가지 구성 요소로 포인터, 길이(len), 용량(cap)이 있다. 여러 슬라이스가 같은 내부 배열을 공유할 수 있으며 배열의 일부를 중복 참조할 수 있다. 슬라이스 타입의 제로 값은 nil이다. 

var s []int		// len(s) == 0, s == nil
s = nil			// len(s) == 0, s == nil
s = []int(nil)	// len(s) == 0, s == nil
s = []int{}		// len(s) == 0, s != nil

make

내장 함수 make는 지정된 원소 타입, 길이, 용량의 슬라이스를 생성한다. 용량 인자는 용량이 길이와 같은 경우 생략할 수 있다. make는 내부적으로 이름 없는 배열 변수를 만들고, 이 배열의 슬라이스를 반환한다.

copy

내장 함수 copy는 실제로 복사된 원소 개수를 반환하며, 이 개수는 두 슬라이스의 길이 중 짧은 것이므로 범위를 넘어서 복사하거나 범위 밖의 위치에 무언가를 덮어쓸 위험은 없다.

append

내장된 append 함수는 하나 이상의 새 원소를 추가하거나 원소의 전체 슬라이스도 추가할 수 있다.

map[K]V

해시 테이블은 순서 없는 키/값 쌍의 모음으로, 모든 키는 별개이며 주어진 키와 관련된 값은 해시 테이블의 크기와 무관하게 평균적으로 일정한 회수의 키 비교를 통해 추출, 갱신, 제거할 수 있다. Go에서 맵은 해시 테이블의 참조이며 맵 타입은 K와 V가 각각 키와 값의 타입일 때 map[K]V로 쓴다.

ages := make(map[string]int) // 문자열에서 정수로 매핑

ages := map[string]int{
    "alice":   31,
    "charlie": 34,
}

ages := make(map[string]int)
ages["alice"] = 31
ages["charlie"] = 34

delete(ages, "alice") // 원소 ages["alice"] 삭제

for name, age := range ages {
    fmt.Printf("%s\t%d\n", name, age)
}

age, ok := ages["bob"]
if !ok { /* ... */ }

if age, ok := ages["bob"]; !ok { /* ... */ }

Go에는 set 타입이 없지만 맵의 키는 유일하므로 맵을 이 목적으로 쓸 수 있다. 때로는 키가 슬라이스인 맵이나 집합이 필요하지만 맵의 키는 비교할 수 있어야 하므로 직접 쓸 수 없고 두 단계를 거쳐서 할 수 있다. 먼저 x와 y가 같다고 간주할 때에만 각 키를 문자열 k(x) == k(y)로 매핑하는 함수를 정의한다. 그리고 키가 문자열인 맵을 생성하고 이 맵을 사용하기 전에 각 키에 함수를 적용한다. 이 방법은 슬라이스 외에 다른 비교할 수 없는 타입에도 사용할 수 있고 비교할 수 있는 키 타입에서도 대소문자 구별 없이 문자열을 비교하는 등, ==가 아닌 동등성을 정의할 때 유용하다. k(x)의 타입이 문자열일 필요는 없고 정수, 배열, 구조체 등 필요한 비교 속성을 가지며 비교할 수 있는 키 타입이면 된다. 

구조체

구조체는 0개 이상의 명명된 임의의 타입 값을 하나의 개체로 모으는 집합형 데이터 타입이다. 각 값은 필드라 한다. 필드들은 하나의 개체로 모이며 개체 단위로 복사, 함수 전달, 반환, 배열에 저장 등을 할 수 있다. 구조체 필드의 이름이 대문자로 시작하면 익스포트된다. 구조체 타입은 익스포트된 필드와 익스포트되지 않은 필드 모두 가질 수 있다.

type Employee struct {
    ID	      int
    Name      string
    Address   string
    DoB	      time.Time
    Position  string
    Salary    int
    ManagerID int
}
var dilbert Employee

dilbert.Salary -= 5000

position := &dilbert.Position
*position = "Senior " + *position

var employeeOfTheMonth *Employee = &dilbert
employeeOfTheMonth.Position += " (proactive team player)"
// => (*employeeOfTheMonth).Position += " (proactive team player)"

구조체의 모든 필드가 비교 가능하다면 구조체 자체도 비교 가능하므로 해당 타입의 두 표현식은 ==나 !=로 비교할 수 있다. Go에서는 타입은 있지만 이름이 없는 필드(익명 필드)를 선언할 수 있다. 필드의 타입은 명명된 타입이거나 명명된 타입의 포인터여야 한다. '익명' 필드에는 묵시적인 이름이 있어서 필드 이름이 충돌하므로 같은 타입의 두 익명 필드를 사용할 수 없다. 그리고 필드 이름은 타입에서 묵시적으로 결정되기 때문에 필드의 가시성도 마찬가지다.

JSON

 자바스크립트 객체 표기법은 구조화된 정보를 보내고 받기 위한 표준 표기법이다. XML, ASN.1, 구글 프로토콜 버퍼도 비슷한 목적으로 만들어졌고 각각의 틈새시장을 갖고 있지만 단순성, 가독성, 보편적 지원으로 인해 JSON이 가장 널리 사용된다. 기본 JSON 타입은 숫자, 불리언, 문자열이며, 문자열은 유니코드 코드 포인트를 큰따옴표로 묶어 표기하고, Go 문법과 유사하게 백슬래시로 이스케이프 처리를 하지만 JSON에서의 \Uhhhh는 룬이 아닌 UTF-16 코드다. 이러한 기본적인 타입은 JSON 배열과 객체를 이용해 재귀적으로 결합할 수 있다. JSON 배열은 순서가 있는 값의 목록으로, 대괄호 안에 쉼표로 구분해 작성한다. JSON 배열은 Go의 배열과 슬라이스를 인코딩하기 위해 사용한다. JSON 객체는 문자열에서 값으로의 매핑이며 name:value 쌍의 목록을 쉼표로 구분하고 중괄호로 둘러싼 형태로 작성한다.

type Movie struct {
    Title string
    Year  int `json:"released"`
    Color bool `json:"color,omitempty"`
    Actors []string
}

마샬링과 언마샬링

Go 데이터 구조를 JSON으로 변환하는 것을 마샬링이라 한다. 마샬링에서는 Go의 구조체 필드명을 JSON 객체의 필드명으로 사용하고 익스포트된 필드만 마샬링된다. 필드 태그는 메타데이터 문자열로 컴파일 시에 구조체의 필드와 연관된다. 필드 태그에는 어떤 문자열도 쓸 수 있지만 통상적으로 공백으로 구분된 key:"value" 쌍으로 해석된다. omitempty 옵션은 필드가 제로 값이거나 비어 있으면 출력하지 않게 한다. 마샬링의 역동작인 JSON을 복호화하고 Go의 데이터 구조에 값을 채우는 것은 언마샬링이라 한다. 마샬링과 마찬가지로 모든 구조체 필드의 이름은 JSON 이름이 대문자가 아니더라도 대문자로 지정해야 한다. 그러나 언마샬링에서 JSON 이름과 Go의 구조체 이름을 연관시킬 때는 대소문자를 구별하지 않으므로 JSON 이름에는 밑줄이 있지만 Go 이름에 밑줄이 없는 경우에만 필드 태그가 필요하다.

type Movie struct {
    Title  string
    Year   int      `json:"released"`
	Color  bool     `json:"color,omitempty"`
    Actors []string
}
var movies = []Movie{
    {Title: "Casablanca", Year: 1942, Color: false, 
     Actors: []string{"Humphrey Bogart", "ingrid Bergman"}},
    {Title: "Bullitt", Year: 1968, Color: true, 
     Actors: []string{"Steve McQueen", "Jacqueline Bisset"}},
    // ...
}
// marshaling
data, err := json.Marshal(movies)
if err != nil {
	log.Fatalf("JSON marshaling failed: %s", err)
}
fmt.Printf("%s\n", data)

data, err = json.MarshalIndent(movies, "", "	")
if err != nil {
	log.Fatalf("JSON marshaling failed: %s", err)
}
fmt.Printf("%s\n", data)
// unmarshaling
var titles []string{ Title string }
if err := json.Unmarshal(data, &titles); err != nil {
	log.Fatalf("JSON unmarshaling failed: %s", err)
}
fmt.Println(titles)

함수

func 이름(파라미터 목록) (결과 목록) {
      본문
}

파라미터 목록은 함수 파라미터의 이름과 타입을 지정하며 이 인자는 호출자가 값이나 인자를 제공하는 지역 변수다. 결과 목록은 함수가 반환하는 값의 타입을 지정한다. 함수가 한개의 이름 없는 결과를 반환하거나 아예 결과를 반환하지 않을 경우 괄호를 사용할 필요가 없으며 보통 생략한다. 다음과 같이 빈 식별자로 파라미터가 사용되지 않는 것을 강조할 수 있다.

func first(x int, _ int) int { return x }

함수 타입은 함수의 시그니처라고도 한다. 두 함수에서 파라미터 목록의 타입이 같고 결과 목록의 타입도 같으면 이 두 함수는 타입 또는 시그니처 값이 같다고 한다. 모든 함수는 호출 시 각 파라미터를 선언한 순서대로 인자로 제공해야 한다. Go에는 파라미터의 기본 값이 없고 인자를 이름으로 지정할 수도 없으므로 파라미터와 결과의 이름은 문서 외에는 호출자에게 영향을 주지 않는다. 함수는 결과를 한 개 이상 반환할 수 있고 반환 값 중 일부를 무시하려면 값을 빈 식별자에 할당하면 된다. 또한 함수의 결과에 이름을 붙이면 반환문에서 피연산자를 생략할 수 있고 이를 단순 반환(bare return)이라 한다. 

오류

기타 여러 함수들에는 개발자가 통제할 수 없는 요인이 있으므로 잘 작성된 프로그램 안에 있더라도 항상 성공이 보장되지는 않는다. 오류는 패키지의 API 또는 애플리케이션의 사용자 인터페이스에서 중요한 부분이며 실패는 여러 예상되는 행동 중 하나에 불과하다. 실패가 예상되는 함수는 보통 마지막에 부가적인 결과를 반환한다. 실패의 원인이 한 가지뿐이라면 결과는 보통 ok라 부르는 불리언 값이고 다양한 원인이 있을 때는 error다. Go는 실패를 예외 처리가 아닌 일반 값으로 보고한다는 점에서 다른 많은 언어와 구별된다. 이 방식에서는 오류 처리 로직에 더 많은 신경을 써야 한다는 점이 명백하며 바로 그 부분이 핵심이다.

익명 함수

명명된 함수는 패키지 수준에서만 선언할 수 있지만 함수 리터럴로 표현식 내의 어디서나 함수 값을 나타낼 수 있다. 함수 리터럴은 함수 선언과 유사하게 작성하지만 func 키워드 뒤에 이름이 없다. 이는 표현식이며 그 값은 익명 함수라 한다. 익명 함수는 전체 구문 환경에 접근할 수 있으므로 내부 함수에서 외부 변수를 참조할 수 있다.

strings.Map(func(r rune) rune { return r + 1 }, "HAL-9000")

func squares() func() int {
    var x int
    return func() int {
    	x++
        return x * x
    }
}

가변 인자 함수

가변 인자 함수는 다양한 개수의 인자로 호출할 수 있다. 가변 인자 함수를 선언하려면 최종 파라미터 타입 앞에 생략 기호 '...'를 붙여서 함수에서 해당 타입의 인자를 제한 없이 받을 수 있다는 것을 나타낸다. 호출자는 묵시적으로 배열을 할당하고 배열에 인자를 복사한 후 함수에 전체 배열의 슬라이스를 전달한다.

func sum(vals ...int) int {
    total := 0
    for _, val := range vals {
    	total += val
    }
    return total
}

fmt.Println(sum())            // 0
fmt.Println(sum(3))           // 3
fmt.Println(sum(1, 2, 3, 4))  // 10

values := []int{1, 2, 3, 4}
fmt.Println(sum(values...))   // 10

연기된 함수 호출

문법상 defer문은 defer 키워드가 앞에 붙은 일반 함수나 메소드 호출이다. 함수와 인자 표현식은 구문이 실행되는 시점에 평가되지만 실제 호출은 defer 구문이 있는 함수가 정적으로 반환문 또는 끝에 도달하거나 비정상적인 패닉이 일어나서 완료될 때까지 미뤄진다. 호출은 개수와 무관하게 지연될 수 있고 지연된 역순으로 실행된다. 연기된 함수는 함수의 실행이 끝날 때까지 실행되지 않으므로 루프 안의 defer문에는 특별히 신경 써야 하고 defer문을 포함한 루프 본문을 반복 시마다 호출되는 다른 함수로 옮기는 것이 방법을 고려해 볼 수 있다.

패닉

Go의 타입 시스템은 컴파일 시 수많은 실수를 잡아내지만 배열 범위 바깥쪽 참조나 nil 포인터 참조 등의 실행 시 검사가 필요한 경우도 있다. Go 런타임이 이러한 실수를 발견하면 패닉을 발생시킨다. 보통 패닉 상황에서는 정상 실행이 중단되고 고루틴에 있는 모든 연기된 함수가 호출되며 프로그램이 로그 메시지와 함께 비정상 종료된다. 모든 패닉이 런타임에 일어나는 것은 아니므로 내장된 패닉 함수를 직접 호출할 수 있다. 패닉은 프로그램이 비정상 종료되게 하므로 일반적으로 프로그램 내에서 논리적 불일치와 같은 중대한 오류에 사용한다. runtime 패키지에는 개발자가 진단 목적으로 패닉 시와 같은 방식으로 스택 전체를 보여주는 기능이 있다.

func main() {
    defer printStack()
    f(3)
}
func printStack() {
    var buf [4096]byte
    n := runtime.Stack(buf[:], false)
    os.Stdout.Write(buf[:n])
}

복구

내장된 recover 함수는 연기된 함수 안에서 호출되며 defer 구문이 들어 있는 함수가 패닉을 일으키면 recover가 현재의 패닉 상태를 끝내고 패닉 값을 반환한다. 패닉을 일으킨 함수는 마지막 부분을 계속하지 않고 정상적으로 반환한다. 다른 때에 recover를 호출하면 아무런 영향 없이 nil을 반환한다.

func Parse(input string) (s *Syntax, err error) {
    defer func() {
        if p := recover(); p != nil {
            err = fmt.Errorf("internal error: %v", p)
        }
    }()
    ...
}
반응형
LIST