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{
log.Fatal("no file specified")
}
f, err := os.Open(os.Args[1])
if err != nil{
log.Fatal(err)
}
defer f.Close()
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
}
}
}
- 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 {
if err == nil {
err = tx.Commit()
}else{
tx.Rollback()
}
}()
_, err = tx.ExecContext(ctx, "INSERT INTO FOO (val) values $1", value1)
if err != nil {
return err
}
return nil
}
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) {
file, err := os.Open(name)
if err != nil {
return nil, nil, err
}
return file, func(){
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()
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로 할지 선택할 수 있다.
- 함수에 변수를 그냥 넘기는 것. (Call by value)
- 함수에 변수의 포인터를 넘기는 것 (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)
- 더 이상 어떤 포인터도 가리키지 않는 데이터.
- 가능하면 필요한 가비지만 만드는 것이 좋다.
- 함수가 실행될 때, 스택 프레임이 함수 데이터를 위해 생성된다.
- 지역 변수들과 함수 인자 들이 여기에 저장된다.
- 이 스택프레임에서 사용되는 스택 포인터를 사용하기 위해 , 선형 데이터(맵, 배열, 슬라이스 등)가 길이와 수용력을 가지고 있는 것이다.
- 스택 포인터가 데이터를 이동할 떄, 선형 데이터의 수용력만큼 넘어간다.
포인터의 단점
- 스택은 함수가 종료될 때 사라지지만, 컴파일러에서 함수가 종료되도 데이터가 사용된다고 판단되면, 스택이아니라 힙에 올리게 되고, 힙은 가비지컬렉터의 처리 대상이 된다.
- 여기서 포인터를 많이 사용하게 되면, 비교적 힙에 들어가는 데이터가 많을 것이고, 이는 가비지컬렉터의 부하 증가를 뜻한다.
- 램(임의 접근 메모리, random access memory)을 빠르게 읽기 위해서는 연속적으로 접근해야 한다.
- GO에서는 구조체 슬라이스는 연속적으로 저장되어있어 빠르게 접근할 수 있다.
- 그러나 항목이 포인터인 구조체는 연속적이지 않고, 포인터를 따라가 데이터에 접근해야 하기 때문에 지연이 생길 수 있다.
7. 타입, 메서드, 인터페이스
7.1 GO의 타입
- 내장 타입
- 구조체를 이용한 사용자 정의 타입
- 구체적인 타입(구체 타입, concrete type)
- 규칙적이고, 딱딱하다고 할 수 있다.
- concrete = specific, sub, 완성(상속)
- 추상 타입 abstract type
- abstact = general, super, 미완성(incomplete)(상속)
- 추상 함수가 있는 것이 추상 클래스이다.
- 추상 함수로만 이루어져 있는 클래스를 순수 추상 클래스라 한다.
7.2 메서드
리시버 (receiver)
- func 키워드와 메서드 이름 사이에 리시버_이름 타입을 괄호로 감싸서 정의
- 관례적으로 타입 이름의 짧은 약어인 첫 문자를 사용 (위의 경우도 Person의 p를 사용하여
p Person
으로 주었다.
7.2.1 포인터 리시버와 값 리시버
- 결정하는 규칙 (p.183)
- 메서드가 리시버를 수정 => 반드시 포인터 리시버 사용
- 메서드가 nil 인스턴스를 처리할 필요 => 반드시 포인터 리시버 사용
- 메서드가 리시버를 수정하지 않음 => 값 리시버 사용 가능
- 이것은 타입에 선언된 다른 메서드에 따라 결정된다.
- 같은 타입에 다른 리시버가 포인터 리시버라면 리시버를 수정하지 않는 메서드라도 포인터 리시버 사용 <= 일관성을 위해