220519

HyeonKi Jo·2022년 5월 19일
0
post-thumbnail

5. 함수

5.4 defer

  • defer로 자원을 해제한다.
  • 정리작업을 수행하는 함수 지정
  • 현재 코드 블록이 끝날 때 자동으로 실행
  • 명령행인자
    • 윈도우에서 exe파일을 실행할 때, main함수에 인자로 데이터를 입력하는 것,
    • hello aaa bbb 이런 식으로, hello실행파일에 aaa와 bbb를 전달한다.
    • 그러나 Go에서는 main에서 인자로 받지 못하고, os를 import하여 os.Args 라는 슬라이스로 받아온다.
  • 예제 1

package main

import (
	"log"
	"os"
	"io"
)

func main() {
	if len(os.Args) < 2{			// 인자에 실행파일의 이름도 들어가기 때문에 값이 1개이면 인자가 없다는 것이다.
		log.Fatal("no file specified")
	}					// 첫번째 인자의 이름을 가진 파일은 연다. 
	f, err := os.Open(os.Args[1])		// f는 os패키지의 파일 객체? 변수? 이다.
	if err != nil{
		log.Fatal(err)
	}
	defer f.Close()				// defer로 f을 바로 닫지 않고, 나중에 닫는다.
	data := make([]byte, 2048)
	for{
		count, err := f.Read(data)	// f(파일)에서 읽은 내용을 data슬라이스에 담는다. 반환값은 읽은 byte수와 에러를 반환한다. count는 파일을 읽을 때 끝을 가리키는 포인터처럼 사용하게 될 것이다.
		os.Stdout.Write(data[:count])	// 슬라이스는 포인터이다. Stdout은 표준 출력을 말한다. 표준출력을 파일로하면 파일로 저장, 프린터로하면 프린터로 출력 된다.  반대로 os.Stdin 도 있는데 흔히 키보드 이다.
		if err != nil {			// 에러가 없다면 넘어간다.
			if err != io.EOF {	// EOF (End Of File)에러이면 다 읽은 것이니 반복문에서 벗어난다. break
				log.Fatal(err)	// 그 외의 에러는 출력한다.
			}
			break
		}
	}
}
  • defer는 여러번 사용될 수 있다. -> 나중에 defer된 함수가 먼저 실행된다. (LIFO)
  • 또 defer는 함수에도 사용될 수 있다.
    • 그러나, defer가 적용된 함수 중 반환값이 있는 함수의 반환값은 따로 읽을 방도가 없다. (의미가 없다.)
  • defer와 이름 지정된 반환값
package main

import (
	"context"
	"database/sql"
)

func DoSomeInserts(ctx context.Context, db *sdql.DB, value1, value2 string) (err error){
	tx, err := db.BeginTx(ctx, nil)
	if err != nil {
		return err
	}

	defer func() int {	// 데이터베이스 함수의 트랜젝션을 defer로 정의한다.
		if err == nil {
			err = tx.Commit()
		}else{
			tx.Rollback()
		}	
		
	}()

	_, err = tx.ExecContext(ctx, "INSERT INTO FOO (val) values $1", value1)	// INSERT명령을 문자열로 입력한다.
	if err != nil {		// 에러가 있다면 err를 반환하며 종료
		return err		// 위 defer로 정의한 익명함수로 들어가 tx.Rollback()이 될 것이다.
	}

	// use tx to do more
	return nil			// 문제없이 종료되었다면, 익명함수로 들어가 tx.Commit() 될 것이다.
}

func main() {
	fmt.Println(example())
}
  • 여기서 tx.Commit()에서도 에러가 날 수 있다.
  • 또, err에 값이 없다면 err가 없는것이 아니라, err가 수정되었다는 것이다. err에는 nil이 있어야 한다.
  • 만약 파일을 함수에서 읽어 온다면, defer로 close를 강제할 수 있다.
package main

import (
	"io"
	"log"
	"os"
)

func getFile(name string) (*os.File, func(), error) {// 명령행 인자로 받아온 파일이름을 받아서 파일과 close함수, err를 리턴하는 함수이다.
	file, err := os.Open(name)	// 파일을 읽어온다.
	if err != nil {				// 읽는데 오류가 있다면 nil, nil, err 반환
		return nil, nil, err
	}
	return file, func(){	// 읽어온 파일, file.close()함수, 에러 를 반환한다.
		file.Close()
	}, err
}	

func main() {
	if len(os.Args) < 2{	// 인자를 받아 인자가 없다면 에러를 출력한다.
		log.Fatal("no file specified")
	}
	f, closer, err := getFile(os.Args[1])
	if err != nil {
		log.Fatal(err)
	}
	defer closer()			// 파일 클로즈를 잊지 않게, closer()를 미리 defer로 예약해놓는다.
	data := make([]byte, 2048)
	for {
		count, err := f.Read(data)
		os.Stdout.Write(data[:count])
		if err != nil{
			if err != io.EOF{
				log.Fatal(err)
			}
			break		
		}
	}
}

5.5 값에 의한 호출을 사용하는 GO

  • call by value: 인자나 반환값은 복사된다.
  • call by refference : 주소가 복사된다.
  • 구조체를 생성하여 함수에서 초기화를 해줬지만, 반환이 없었고, 구조체 p의 주소를 보낸것이 아닌 값에의한 호출이었기 때문에 초기화 되지 않음을 확인할 수 있다.
    • 파라미터로 넘어온 맵에 대한 변경은 원본에도 적용
  • 슬라이스
    • 길이 조정 외의 모든 조작이 원본에도 적용된다.
    • 그러나 새로운 값을 넣는것은 안된다....
  • 맵과 슬라이스는 포인터로 구현이 되었기 때문에 원본도 변경되는 것이다.

6. 포인터

6.1 빠른 포인터 입문

  • 포인터란?
    • 값이 저장된 메모리의 위치 값(주소)를 저장하는 변수
  • 주소 연산자(&)
  • rkswjq dustkswk(*, dereferencing operator)
  • 모든 포인터는 어떤 타입을 가리키던 간에 항상 같은 크기를 가진다.
  • 포인터의 제로 값: nil
    • 슬라이스, 맵, 함수, 채널, 인터페이스의 제로 값: nil
    • 위 멤버들은 포인터로 되어있다고 볼 수 있다.
  • nil은 숫자 0이 아니다.
    • nil을 숫자로 변경/변환하거나 숫자를 반대로 변환할 수 없다.
  • 포인터는 변수의 주소값을 가리키면서, 또 스스로의 주소를 가지고 있다.
    • 만약 포인터가 가리키는 변수의 값을 보고싶다면 *을 붙여 값을 확인할 수 있다.
    • 또, 포인터 스스로 주소를 가지고 있으니, 포인터의 포인터도 생성할 수 있다.
    • 이때, 값을 확인하려면 *을 두번 붙여야 한다.
  • 포인터 타입
    • 포인터가 가리키는 값의 타입
    • 포인터가 어떤 타입의 값을 가리키는지 나타냄.
    • 타입 이름 앞에 *
    • 예) var pointerToX *int
  • 포인터가 nil일때 값에 접근하면 패닉이 출력된다.

    포인터 타입
  • p를 초기화 할 때, Middlename이 포인터기 때문에, &"Perry"로 넣엇지만, 에러가 출력된다.
  • 기본 타입의 리터럴이나 상수는 주소가 없으므로, 주소연산자를 사용할 수 없다.
    • 헬퍼함수를 이용하여 이 문제를 해결하자.
  • 첫번쨰 해결방법
    • 변수로 선언한 후, 주소를 반환한다.
  • 두번째 해결방법
    • 헬퍼함수를 하나 선언하여 인자로 문자열을 넣고 주소값을 반환한다.

6.2 포인터를 두려워 말라

  • 포인터는 실제 클래스의 동작과 비슷하다.
  • JAVA의 데이터 타입
    • 원시 타입 (Primitive Type): 8개
      • byte, short, int, long, float, double, bool, char : 객체가 아님, call by value
    • 클래스 : 객체를 참조 변수로 참조해서 작업, call by reference
    • 원시 타입의 클래스 : Byte, short, Integer, Long, Float, Double, Boolean, Character
    • 객체의 연결이 끊어지면, 가비지 컬렉터에 의해 버려진다.
    • 클래스의 객체를 함수로 넘기고 해당 클래스 내의 항목 값을 수정하면, 해당 변경은 전달된 변수에 반영이 된다.
    • 파라미터로 재할당이 되면, 해당 변경은 전달된 변수에 반영되지 않는다.

6.3 포인터는 변경 가능한 파라미터를 가리킨다.

  • GO는 변수를 Call by value로 할지 Call by reference로 할지 선택할 수 있다.
      1. 함수에 변수를 그냥 넘기는 것. (Call by value)
      1. 함수에 변수의 포인터를 넘기는 것 (Call by reference)
  • 비 포인터 타입: 기본타입, 배열, 구조체 => 원본의 불변성
  • 포인터: 함수로 전달되면 포인터의 복사본이 생성 => 원본에 닿을 수 있다. 수정할 수 있다.
  • int형 포인터 f를 선언하고, 함수에서 값을 할당하여 그 주소를 넣었지만, 출력은 그대로 nil이다.
  • 그래서 f에 값이 있는 주소를 넣기 위해, 함수에서 *g = x로 값을 주었다.
    • 그러나 f가 제로값 (nil)이기 떄문에 패닉이 출력된다.

6.4 포인터는 최후의 수단

  • 포인터
    • 데이터 흐름 이해를 어렵게 한다.
    • GC (garbage collector)에게 추가 작업 부하를 건다. (포인터를 이용하면 주로 힙을 이용)
      • 가비지는 힙에 생성

6.5 포인터로 성능 개선

  • 포인터는 모든 데이터 타입을 함수로 전달할 떄 상수시간이 걸린다.
    • 주소를 함수에 전달하므로, 모든 데이터 타입의 주소 길이는 같다.
    • 데이터가 1Mb 이상이 될 때, 포인터로 넘기는 것이 더 빠르다.
      • 100byte(10나노초) < 포인터(30나노초)
      • 10Mb(2밀리초) > 포인터(0.5밀리초)

6.6 제로 값과 값없음의 차이

  • 0, ' ' VS NULL, '' => GO : nil
  • 포인터를 이용하여 변수나 구조체의 항목의 값이 제로 값인지 nil, 없는 값인지 구분하는데 사용한다.
  • 할당되지 않은 변수나 구조체 항목에 nil포인터를 사용
  • 또한, 포인터는 변경가능함을 나타내므로 함수에서 nil포인터를 직접 반환하는 것보다 콤마 OK 관용구를 사용하자.
  • nil 포인터를 함수의 파라미터나 구조체의 항목의 값으로 담아서 함수의 인자로 넘기면, nil포인터를 통해서는 값을 저장할 수 없으므로, 함수 안에서 값을 설정(간접 연산자 *를 사용하는 것)할 수가 없음을 명심한다.

6.7 맵과 슬라이스의 차이

  • 기본적으로 함수에 의해 수정을 할 수 없다고 가정하자. 만약 슬라이스의 내용을 수정한다면 함수의 문서에 꼭 넣어두도록 하자.

6.8 버퍼 슬라이스

  • 메모리 용도에 따른 구분
    • 버퍼 (buffer) 완충장치..?
    • 캐시 (cache)
    • 풀 (pool)
  • 버퍼의 용도로 슬라이스를 사용할 때, data를 make함수로 일정한 크기로 슬라이스를 만들고, file.Read(data)로 읽는다.
    • 이때, count에 읽은 byte수를 이용해 process시 읽은 만큼만 처리하게 구성할 수 있다.

6.9 가비지 컬렉션 작업량 줄이기

  • 버퍼를 이용하면 GC의 부하를 줄일 수 있다.
  • 가비지(Garbage)
    • 더 이상 어떤 포인터도 가리키지 않는 데이터.
    • 가능하면 필요한 가비지만 만드는 것이 좋다.
  • 함수가 실행될 때, 스택 프레임이 함수 데이터를 위해 생성된다.
    • 지역 변수들과 함수 인자 들이 여기에 저장된다.
    • 이 스택프레임에서 사용되는 스택 포인터를 사용하기 위해 , 선형 데이터(맵, 배열, 슬라이스 등)가 길이와 수용력을 가지고 있는 것이다.
    • 스택 포인터가 데이터를 이동할 떄, 선형 데이터의 수용력만큼 넘어간다.

포인터의 단점

    1. 스택은 함수가 종료될 때 사라지지만, 컴파일러에서 함수가 종료되도 데이터가 사용된다고 판단되면, 스택이아니라 힙에 올리게 되고, 힙은 가비지컬렉터의 처리 대상이 된다.
    • 여기서 포인터를 많이 사용하게 되면, 비교적 힙에 들어가는 데이터가 많을 것이고, 이는 가비지컬렉터의 부하 증가를 뜻한다.
    1. 램(임의 접근 메모리, random access memory)을 빠르게 읽기 위해서는 연속적으로 접근해야 한다.
    • GO에서는 구조체 슬라이스는 연속적으로 저장되어있어 빠르게 접근할 수 있다.
    • 그러나 항목이 포인터인 구조체는 연속적이지 않고, 포인터를 따라가 데이터에 접근해야 하기 때문에 지연이 생길 수 있다.

7. 타입, 메서드, 인터페이스

7.1 GO의 타입

  • 내장 타입
    • 기본 타입, 복합 타입
  • 구조체를 이용한 사용자 정의 타입
  • 구체적인 타입(구체 타입, concrete type)
    • 규칙적이고, 딱딱하다고 할 수 있다.
    • concrete = specific, sub, 완성(상속)
  • 추상 타입 abstract type
    • abstact = general, super, 미완성(incomplete)(상속)
    • 추상 함수가 있는 것이 추상 클래스이다.
    • 추상 함수로만 이루어져 있는 클래스를 순수 추상 클래스라 한다.

7.2 메서드

  • 타입(type)을 위한 메서드
    • 반드시 패키지 블록 레벨에서 정의해야 함

리시버 (receiver)

  • func 키워드와 메서드 이름 사이에 리시버_이름 타입을 괄호로 감싸서 정의
  • 관례적으로 타입 이름의 짧은 약어인 첫 문자를 사용 (위의 경우도 Person의 p를 사용하여 p Person으로 주었다.

7.2.1 포인터 리시버와 값 리시버

  • 결정하는 규칙 (p.183)
    • 메서드가 리시버를 수정 => 반드시 포인터 리시버 사용
    • 메서드가 nil 인스턴스를 처리할 필요 => 반드시 포인터 리시버 사용
    • 메서드가 리시버를 수정하지 않음 => 값 리시버 사용 가능
      • 이것은 타입에 선언된 다른 메서드에 따라 결정된다.
      • 같은 타입에 다른 리시버가 포인터 리시버라면 리시버를 수정하지 않는 메서드라도 포인터 리시버 사용 <= 일관성을 위해
profile
Talking Potato

0개의 댓글