일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- ALB
- vscode
- Kind
- ubuntu
- Docker
- 중첩가상화
- go
- 워커노드
- 쿠버네티스
- network namespace
- windows
- nested virtualization
- 패스워드 재설정
- ansible
- curl
- 묘공단
- Pane
- 네트워크 네임스페이스
- 컨테이너
- 도커
- code-server
- 504
- WSL
- kubernetes
- calico
- web ide
- kubernets
- passwd
- 코어 쿠버네티스
- containerd
- Today
- Total
a story
Go스터디: 2주차(3~11장) 본문
이 글은 골든래빗 ‘Tucker의 Go 언어 프로그래밍의 3~11장 써머리입니다.
이 책은 다른 언어에 익숙한 분들은 go의 특성을 이해할 수 있고, 다른 언어에 대한 이해가 없어도 프로그래밍에 대한 기본을 이해하기 쉽게 쓰여진 장점이 있습니다.
03 Hello Go World
03.01. Go에서 코드 실행 단계
코드가 프로그램이 되어 실행되기 까지 5가지 단계를 거쳐야 한다.
- 폴더 생성
- 모든 코드는 패키지 단위로 작성된다. 같은 폴더에 위치한 .go 파일은 모두 같은 패키지에 포함되고, 패키지 명으로 폴더명을 사용한다.
- 예를 들어, goproject/hello/extra
- 여기서 hello 폴더에 든 .go 파일은 hello 패키지가 된다.
- extra 폴더에 든 .go 패키지는 extra 패키지가 된다.
- .go 파일 생성 및 작성
- Go 모듈 생성 (→ 16장)
- go 1.16 버전 이후로 go 모듈이 기본으로 적용된다. 그러므로, go 코드는 빌드하기 전에 모듈을 생성해야 한다.
- 모듈 생성: go mod init <모듈 이름>
- go mod init goproject/hello
- go 모듈을 생성하면 go.mod 파일이 생성되고, 여기에 모듈명, go버전, 필요한 패키지 목록 정보가 저장된다.
- 빌드
- go build 명령으로 go 코드를 기계어로 변환해 실행 파일을 만든다.
- GOOS, GOARCH 환경변수로, 다른 환경에 실행되는 실행 파일 만들 수 있다.
- GOOS=linux GOARCH=amd64 go build
- go tool disk list 로 설정 가능한 값을 확인
- 실행
- 실행 파일을 명령어로 실행
03.02. 샘플 코드 설명
package main // 패키지 선언: 이 코드가 어떤 패키지에 속하는지 알려줌
// main 패키지는 프로그램 시작점(entry point)을 포함하는 특별한 패키지
import "fmt" // 패키지 가져온다. fmt는 표준 입출력을 다루는 내장 패키지 -> 5장
func main() { // main 함수 시작, main() 함수는 프로그램 진입점 함수
// Hello Go World 출력 _ // 는 주석 예약어
// go 규약: 다른 프로그램에서 쓰이는 함수 앞에 함수명으로 시작하는 주석을 달아 함수를 설명
// 한줄 주석 /* */ 여러줄 주석
fmt.Println("Hello Go World") // 표준 출력(터미널 화면)으로 문자열을 출력 함수
} // 코드 블록 종료, 여기서는 main 함수 블록 종료
04 변수
04.01. 변수 기본
변수(Variable)는 ‘값을 저장하는 메모리 공간을 가리키는 이름’이다.
컴퓨터 입장에서 프로그램은 ‘메모리에 있는 데이터를 언제 어떻게 변경할지 나타낸 문서’이다
그래서 데이터를 어떻게 저장하고, 조작(선언, 사용, 출력)하는지가 핵심이 된다.
go에서 변수는 아래와 같이 선언한다. (메모리 할당이라고 부른다)
var a int = 10 // 변수선언 키워드, 변수명, 타입 = 초기값
변수는 4가지의 속성을 가진다.
- 이름: 프로그래머는 이름을 통해 메모리 공간을 접근, 이름은 camel case 를 사용하는 것이 권장, 변수가 함수 내부가 아닌 외부에 선언되어 있고, 변수명이 대문자로 시작하면 이 변수는 패키지 외부로 공개된다
- 값: 메모리 공간에 저장된 값
- 주소: 메모리 공간의 시작 주소
- 타입: 변수 값의 형태(정수, 실수, 문자열, ..), 실제로 메모리 시작 주소에서 타입 크기에 해당하는 공간을 할당한다
변수 선언은 선언 대입문을 사용할 수 있다 _ var 키워드와 타입을 생략해 변수를 선언
var b = 3.1415
c := 365
s := "hello"
근데 같은 변수에 대해서 :=를 또 사용하면 아래와 같은 에러가 발생한다. NO NEW VAR!!
.\\5.1.go:18:9: no new variables on left side of :=
go는 강 타입 언어이기 때문에 연산이나 대입에서 타입이 다르면 에러가 발생한다.
a := 3
b := 3.5
var c int = b // cannot use b (variable of type float64) as int value in variable declaration
d := a * b // invalid operation: a * b (mismatched types int and float64)
타입을 변환하려면 원하는 타입명을 적고 ()로 변환할 수 있는데, 이럴 때 값이 변경될 수 있다
a := 3
b := 3.5
d := a * int(b)
fmt.Println(d) // 9
전역 변수(global variable)는 같은 패키지 내에서 언제나 접근할 수 있지만, 지역 변수(local variable)의 범위는 {} 안에서만 유효하다.
04.02. 숫자의 표현
정수의 표현에서 부호를 표현하기 위해 부호 비트를 이용한다. 단 값도 2의 보수(비트를 반전하고 +1)를 사용해 표현한다.
실수의 표현을 위해 부호비트(1비트), 지수부(8비트), 소수부(23비트)를 사용한다. 실수 타입은 유효자리 수가 정해져 있으므로 데이터 처리에 유의해야 한다.
05 fmt 패키지를 이용한 텍스트 입출력
05.01 입출력 기본
프로그램과 사용자는 입력과 출력을 통해서 상호작용을 하는데, 기본적으로는 입력을 위해 키보드, 출력을 위해 화면을 이용한다. 모든 입력과 출력을 프로그램에서 구현하기는 복잡하기 때문에, 운영체제가 제공하는 표준 입출력 스트림(standard input/output stream)을 사용하면, 프로그램 내부에서 입출력을 간편하게 처리할 수 있다.
go에서는 fmt 패키지를 이용한다.
fmt의 표준 출력 함수
Print() 함수 입력값을 출력한다
Println() | 함수 입력값을 출력+개행한다 |
Printf() | 함수 입력값을 서식 문자를 이용한 출력한다 |
fmt의 표준 입력 함수
Scan() 표준 입력에서 값을 입력 받는다
Scanf() | 표준 입력에서 서식 문자로 입력 받는다 |
Scanln() | 표준 입력에서 한 줄을 읽어서 값을 받는다 |
Scan()
변수들의 메모리 주소를 인수로 받는다. _ 여러 값을 입력받을 때 입력 값은 공백이나 줄바꿈으로 구분한다.
반환값은 성공적으로 입력한 개수와 입력 실패 시 에러를 반환한다.
func Scan(a ...interface{}) (n int, err error)
반환값의 의미를 아래 예시로 살펴본다.
go run .\\5.2.go
1 hello // 입력
1 expected integer // 1개 받고, err
go run .\\5.2.go
hello 1 // 입력
0 expected integer // 0개 받고 err
Scanf() 함수 원형
func Scanf(format string, a ...interface{}) (n int, err error)
05.02 키보드 입력과 Scan()의 동작 원리
사용자가 표준 입력 장치로 입력하면 입력 데이터는 컴퓨터 내부에 표준 입출력 스트림(standard input stream)이라는 메모리 공간에 미시 저장되는데, Scan()함수들은 그 표준 입력 스트림에서 값을 읽어서 입력값을 처리한다.
Stream 이란 흐름의 의미이다. 즉, 입력 데이터가 연속된 데이터 흐름 형태를 가지고 있다는 뜻이다. 한편 한번 읽은 데이터를 다시 읽을 수 없다는 의미도 포함한다.
이때 먼저 입력한 데이터 부터 읽어오기 때문에 데이터는 거꾸로 저장된다. (FIFO 구조)
다만 이러한 구조 때문에 var int a 에 대해서 Scan(&a)으로 Hello를 읽으면 H 먼저 읽게 되는데, 이러한 구조 때문에 첫번째 캐릭터에서 에러가 발생하면 표준 입력 스트림에 불필요한 입력이 남게된다(여기서는 ello)
그래서 여러 번 Scan() 함수를 호출할 때는 이러한 문제가 있으므로, 표준 입력 스트림을 비워줄 필요가 있다.
package main
import (
"bufio" // io를 담당하는 패키지
"fmt"
"os" // 표준 입출력 등을 가지고 있는 패키지
)
func main() {
stdin := bufio.NewReader(os.Stdin) // 표준 입력을 읽는 객체
var a int
var b int
n, err := fmt.Scan(&a, &b)
if err != nil {
fmt.Println(n, err)
stdin.ReadString('\\n') // 줄바꿈 문자가 나올때까지 읽는다 (표준 입력 스트림 비워짐)
} else {
fmt.Println(n, a, b)
}
n, err = fmt.Scan(&a, &b)
if err != nil {
fmt.Println(n, err)
} else {
fmt.Println(n, a, b)
}
}
결과 확인
// stdin.ReadString('\\n') 없을 때
PS C:\\Users\\montauk\\Desktop\\projects\\goprojects\\go05> go run .\\5.1.go
1 Hello
1 expected integer
0 expected integer
// stdin.ReadString('\\n') 있을 때
PS C:\\Users\\montauk\\Desktop\\projects\\goprojects\\go05> go run .\\5.1.go
1 Hello
1 expected integer
2 3
2 2 3
bufio는 입력 스트림으로 부터 한 줄을 읽는 Reader 객체를 제공한다.
func NewReader(rd io.Reder) *Reader
06 연산자
산술연산자(사칙연산자, 비트 연산자, 시프트 연산자), 비교연산자(==, != , <, >), 논리 연산자(&&, ||, !)
대입 연산자(=)는 우변값을 좌변(메모리 공간)에 복사한다.
- 좌변에는 반드시 저장할 공간이 있는 변수가 와야 한다.
- 대입연산자는 값을 반환하지 않는다.
- 복수 대입연산자 사용 가능(a, b = 3, 4) 하다.
기타 연산자
[] 배열의 요소에 접근
. | 구조체나 패키지 요소에 접근 |
& | 변수의 메모리 주소값을 반환 |
* | 포인터 변수가 가리키는 메모리 주소에 접근 |
… | 슬라이스 요소에 접근하거나, 가변 인수를 만들 때 사용 |
: | 배열의 일부분을 집어올 때 사용 |
← | 채널에서 값을 빼거나 넣을 때 사용 |
07 함수
함수는 함수 키워드, 함수명, 매개변수, 반환타입, 함수 코드 블록으로 구성된다.
func Add(a int, b int) int {
// code block
}
함수를 호출할 때 입력하는 변수를 argument(아규먼트, 인수)라고 한다.
함수가 외부로 입력 받는 변수를 parameter(매개변수, 파라미터)라고 한다.
c := Add(3, 4) // 함수 호출 부에서 3, 4는 인수
func Add(a int, b int) int { // 함수 정의 부의 a, b는 매개변수
return a+b
}
함수를 호출하며 입력한 값은 실제 함수에 어떻게 전달되는가?
인수는 매개변수로 복사된다. 매개변수와 함수 내 선언된 변수는 함수가 종료되면 변수 범위를 벗어나서 접근하지 못한다.
보낸 값을 그대로 사용하지 않고, 값을 복사해서 사용한다. 이 것은 함수 내에서 선언한 a, b 변수에 초기값을 대입하는 것과 깉다.
위 예시에서, 3,4는 복사되어 매개변수에 전달되고, return 값은 복사되어 반환된다. 호출한 함수가 종료되면 함수에서 사용한 지역 변수에 접근할 수 없다.
return으로 함수 결과가 반환 되면서 함수가 즉시 종료되고, 함수를 호출했던 호출 위치로 명령 포인터(instruction pointer)가 되돌아가 수행을 이어간다.
08 상수
상수는 변하지 않는 값을 말한다. 기본 타입이(primitive) 아닌 타입(complex)은 상수를 사용할 수 없다.
const ContVal int = 10
상수를 변경하려고 할 때 발생하는 에러
cannot assign to c (neither addressable nor a map index expression)
상수를 메모리 접근할 때 발생하는 에러
invalid operation: cannot take address of c (constant 10 of type int)
상수는 초기화된 값이 변하지 않는다. 좌변에 올 수 없다. 상수는 메모리 주소값을 접근할 수 없다.
왜? 상수는 컴파일 할 때 숫자 자체로 치환되나?
→ 184P. 상수의 메모리 주솟갑셍 접근할 수 없는 이유 역시 컴파일 타임에 리터럴로 전환되어서 실행 파일에 값 형태로 쓰이기 때문이다. (동적 메모리 할당 영역 사용하지 않음)
프로그램이 로드될 때, 실행 파일이 올라간 영역을 코드 영역이라 하고, 프로그램 실행을 위해서 실행 중 할당해서 사용되는 영역을 동적 할당 메모리 영역이라고 한다. 상수는 리터럴로 코드 영역에 포함되기 때문에 동적 할당 영역을 사용 안함.
상수는 언제 사용하나?
- 변하면 안되는 값
- 코드 값: 어떤 숫자에 의미를 부여하는 것 (ex. 404: NOT FOUND)
- iota 로 간편하게 열거값 사용하기 (iota 키워드를 사용하면 1로 증가하는 코드값 생성 한다)
리터럴(literal)이란 고정된 값, 값 자체로 쓰인 문구라고 볼 수 있다. go에서 상수는 리터럴과 같이 취급한다. 그래서 컴파일 될 때 상수는 리터럴로 변환되어 실행 파일에 쓰인다.
09~11 if, switch, for
09. If
기본형
if 조건문 {
문장
} else if 조건문 {
문장
} else {
문장
}
쇼트 서킷(short circuit)? &&와 ||의 특성으로, 우변을 체크하지 않을 수 있음
- && 연산은 좌변이 false이면 우변을 검사하지 않고 false 처리한다.
- || 연산은 좌변이 true 이면 우변은 검사하지 않고 true 처리한다.
if 초기문 사용 예시 (단, 초기문의 변수의 범위는 if 문에서만 사용된다)
if filename, success := UploadFile(); success {
fmt.Println("success:", filename)
} else {
fmt.Println("fail")
}
10. switch
기본형
switch 비교값 {
case 값1:
문장
case 값2:
문장
default:
문장
}
go의 switch 문의 각 case 종료 시에 break 가 없어도 빠져 나간다. 다음 case 문까지 체크하고 싶다면 fallthrough 키워드를 사용할 수 있다. (단, 혼동을 일으킬 수 있으므로 fallthrough를 권장하지 않음)
11. for
기본형
for 초기문; 조건문; 후처리 {
코드블록
}
생략형
for ; 조건문; 후처리 {}
for 초기문; 후처리 {}
for ; 조건문; {}
for 조건문 {}
중첩 for문에서 flag 변수로 break를 처리하기 → 더 복잡한 경우는 label을 사용할 수 있다.
'Book Study > Tucker의 Go Programming' 카테고리의 다른 글
Go스터디: 7주차(31장) (0) | 2023.11.09 |
---|---|
Go스터디: 6주차(27~30장) (0) | 2023.11.05 |
Go스터디: 5주차(23~26장) (0) | 2023.10.29 |
Go스터디: 4주차(18~22장) (1) | 2023.10.22 |
Go스터디: 3주차(12~17장) (1) | 2023.10.15 |