Golang vol.1 (기본 문법)
개요
고랭 학습하면서 느낀점이나 기록하면 좋을법한 내용들을 정리
프로젝트 시작하기
이하의 사이트에서 OS에 따라 설치를 진행해준다.
Downloads - The Go Programming Language
Downloads After downloading a binary release suitable for your system, please follow the installation instructions. If you are building from source, follow the source installation instructions. See the release history for more information about Go releases
go.dev
설치가 끝나면 mac의 경우에는 (local)user/go 라는 디렉터리가 생기고 그 안에 go get을 이용한 패키지를 설치할 때 이 go라는 디렉터리 안에 저장되며 각각의 프로젝트에서 설치한 go패키지들을 어떻게 이 user/go를 바라볼 수 있게 하는지는 GOPATH라는 환경변수를 이용해서 바라보게 한다.
만들고 싶은 프로젝트 디렉터리에서 아래의 명령어를 치면 go.mod라는 파일이 생성되는데 여기에는 go의 버전과 이 프로젝트에서 사용하고 있는 package들이 적혀있다. (이 이름이 나중에 루트디렉터리 같은 역할도 한다.)
go mod init 이름
그리고 main.go를 만들어 보자
package main
import "fmt"
func main() {
fmt.Println("Hellow World")
}
go run main.go
:= 와 = 의 차이?
:= 선언, 양도 및 재선언을위한 것이고, 변수의 유형도 자동으로 추론한다.
예를 들어, foo := 74과 밑의 나열들과 똑같은 의미이다. (참고로 함수내에서만 := 을 사용할 수 있다.)
// 1
var foo int
foo = 74
// 2
var foo int = 74
// 3
var foo = 74
// 1,2,3은 이하의 코드로 대체할 수 있다.
foo := 74
주의할 점은 :=을 이용해 이미 존재하던 변수에 재선언을 하는 경우 variables scope를 주의해야한다.
이전에 if else로 분기처리로 로직을 구성하고 if 안에 어떤 변수에 := 을 썼기 때문에 변수를 재선언을 한것이 됬는데
그 재선언한 값으로 들어간 친구를 if 밖에서도 불러내려고 할 때 값이 계속 비어있어서 뭐지? 라고 삽질을 좀 했었다.
func CreateOrUpdateCart(c *fiber.Ctx) error {
var cart models.Cart
var request CreateOrUpdateOrderRequest
if err := c.BodyParser(&request); err != nil {
return err
}
db.DB.Where("title = ?", request.Title).Find(&cart)
if cart.Id == 0 {
// 여기서 cart를 재선언해서 cart가 if문 내부의 스코프로 되어버렸다.
cart := models.Cart{
Title: request.Title,
Description: request.Description,
Price: uint(request.Price),
Amount: uint(request.Amount),
}
db.DB.Model(&cart).Create(&cart)
} else {
// update
cart.Amount = cart.Amount + uint(request.Amount)
db.DB.Model(&cart).Updates(&cart)
}
fmt.Printf("%s", cart.Title) // create의 cart는 빈값으로 출력
return c.JSON(cart)
}
함수의 이름을 PascalCase camelCase로 했을 때의 차이
Go 언어에서는 함수의 이름에 따라 해당 함수의 가시성이 달라진다
- 파스칼 케이스 (PascalCase): 함수의 이름이 대문자로 시작하는 경우 (예: MyFunction), 해당 함수는 공개 (public) 함수가 된다. 이는 다른 패키지에서도 이 함수를 호출할 수 있음을 의미
- 카멜 케이스 (camelCase): 함수의 이름이 소문자로 시작하는 경우 (예: myFunction), 해당 함수는 비공개 (private) 함수가 되며 이는 해당 함수가 선언된 패키지 내에서만 호출될 수 있음을 의미
함수는 복수의 값을 리턴할 수 있다.
이 특징이 다른 언어와 꽤 차이를 보이는데 이하의 코드를 보면 lenAndUpper라는 값은 두개의 값을 동시에 리턴한다.
Go에서는 이런식으로도 코딩이 가능하다.
func lenAndUpper(str string) (int, string) {
return len(str), strings.ToUpper(str)
}
func main() {
leng, upperStr := lenAndUpper("heewon")
fmt.Println(leng, upperStr)
}
함수는 가변하는 인자를 받을 수 있다.
가변인자는 인자의 갯수가 보통 함수는 정해져 있는데 그에 관계없이 한개 또는 복수의 인자를 보내도 함수를 사용할 수 있다
func variableArgs(str ...string) {
fmt.Println(str)
}
func main() {
variableArgs("gwak", "hee", "won") // output: [gwak hee won]
variableArgs("gwakheewon") // output: [gwakheewon]
}
함수의 실행이 끝났을 때 어떤 동작을 예약할 수 있는 예약어 defer
func variableArgs(str ...string) {
defer fmt.Println("is done!")
fmt.Println(str)
}
func main() {
variableArgs("gwak", "hee", "won")
variableArgs("gwakheewon")
}
* 포인터와 & 래퍼런스를 쓴다(C언어 추억)
Go 언어에서 포인터는 메모리 주소를 참조하고, 그 주소에 저장된 데이터를 조작할 수 있는 강력한 기능이다.
포인터를 사용하면 함수에서 여러 값을 반환하지 않고도 복잡한 데이터 구조를 효율적으로 다룰 수 있다. 예를 들어, 큰 구조체를 함수에 전달할 때 포인터를 사용하면 데이터의 복사본을 만들지 않고 원본 데이터를 직접 조작할 수 있어 메모리 사용을 최소화할 수 있습니다.
포인터 사용의 기본은 & 연산자로 변수의 주소를 얻고, * 연산자로 해당 주소의 데이터에 접근하는 것이다. 밑의 예제를 살펴보자
package main
import "fmt"
func main() {
a := 2
b := a
a = 10
fmt.Println(a, b) // 10, 2
c := 2
d := &c
c = 10
fmt.Println(c, *d) // 10, 10
}
- b값에 a를 그대로 대입하면 데이터의 복사가 일어나면서 새로운 변수가 메모리에 할당된다. b는 새롭게 생성된 변수이기 때문에 a를 변경해도 b에는 영향이 없다
- d값에 c의 주소값을 대입하면 데이터의 복사가 일어나지 않아서 메모리를 절약한다. d는 c의 주소값을 보고 있기 때문에 c값을 변경하면 *d의 값도 변경된다.
Array와 Slice
go에서는 배열을 어떻게 사용할까?
name := [5]string{"gwak", "heewon", "won"}
name[3] = "천"
name[4] = "재"
fmt.Println(name) // [gwak heewon won 천 재]
위와 같이 배열을 선언할 때는 배열의 길이를 미리 정의해 두어야 한다.
그리고 그 길이 이상의 데이터를 넣으려고 하면 에러가 발생한다.
name[5] = "다" // invalid argument: index 5 out of bounds [0:5]
동적인 길이를 같이는 배열을 선언하기 위해서 slice라는 친구를 쓰는데
밑과 같이 배열의 길이 부분을 빈칸으로써 선언하면 그게 slice가 된다.
slice := []string{"gwak", "heewon", "won"}
slice = append(slice, "천")
slice = append(slice, "재")
slice = append(slice, "다")
fmt.Println(slice) // [gwak heewon won 천 재 다]
한가지 주의할 점은 배열에 데이터를 추가할 때 append라는 함수를 이용해 새롭게 반환된 배열을 재할당 해줘야 한다.
※잘못된 예시
slice[6] = 'test // invalid argument: index 6 out of bounds [0:6]
Map
Map이라는 javscript의 Object와 비슷하지만 키와 값의 타입이 정해져 있어서 좀 덜 유연한 Object라고 할 수 있다.
map_test := map[string]string{"name": "heewon", "age": "23"}
for key, value := range map_test {
fmt.Println(key, value)
}
// 출력 결과
// name heewon
// age 23
Struct
Map보다 유연한 형태의 데이터를 선언할 수 있는 친구가 Struct이다.
밑과 같은 형태로 사용할 수 있다.
type person struct {
Name string
age int
FavFood []string
}
favFood := []string{"bread", "chicken"}
heewon := person{
Name: "heewon",
age: 23,
FavFood: favFood,
}
fmt.Println(heewon)
여기서 주목해야할 부분은 멤버변수중에 대문자로 시작하는 변수와 소문자로 시작하는 변수가 있는데 이는 go의 public private을 나누는 방법으로 대문자는 public 소문자는 private이 된다.
조금 더 struct의 사용방법에 대해 알아보자
type Account struct {
owner string
balance int
}
// NewAccount creates Account
func NewAccount(owner string) *Account {
account := Account{owner: owner, balance: 0}
return &account
}
- 어떤 계좌의 구조체가 있는데 owner와 balance를 private으로 함으로써 외부에서 접근이 불가능하게 설정한다.
- NewAccount라는 생성자와 같은 역할을 하는 함수를 선언하고 이는 &account를 리턴함으로써 값을 복사하지 않고 이미 생성된 변수의 주소값을 보낸다.(Go에서 많이 쓰이는 패턴이라고 한다.)
// Deposit x amount on your account
func (a *Account) Deposit(amount int) {
a.balance += amount
}
func (a *Account) Withdraw(amount int) error {
if a.balance < amount {
return errors.New("can't withdraw you are poor")
}
a.balance -= amount
return nil
}
func (a Account) Balance() int {
return a.balance
}
- a Account는 리시버라는 개념으로 이렇게 선언하면 Account의 멤버변수가 되고 account.Balance()와 같은 형태로 사용할 수 있따.
- 여기서 a *Account와 a Account의 차이는 역시 리시버로 설정한 변수를 값을 복사해서 받을 것이냐 아니면 참조형태로 받아서 값을 복사하지 않을꺼냐의 차이가 있고 NewAccount로 생성한 계좌에 직접 변경이 필요하다면 a *Account로 리시버를 설정해야 한다.
Goroutines
Go언어는 동시성을 지원하는 강력한 기능을 제공하는데 그 중심에 고루틴(Goroutines)와 채널(Channels)이 있다. 이 두 기능을 통해 쉽고 효과적으로 동시에 여러 작업을 수행하고 데이터를 안전하게 교환할 수 있다.
고루틴은 함수나 메서드를 동시에 실행할 수 있게 해주며, go 라는 키워드를 사용해 간단하게 시작할 수 있다.
func say(s string) {
for i := 0; i < 5; i++ {
fmt.Println(s)
time.Sleep(time.Millisecond * 100)
}
}
func main() {
go say("world")
say("hello")
}
- go say로 비동기적으로 함수를 실행한다.
- main함수에서 say함수를 두번 실행하는데 두번다 go를 적지않은 이유는 두개다 비동기적으로 실행되면 main함수는 즉시종료되며 고루틴으로 실행중이던 함수도 종료되어 버린다.
Channels
채널은 고루틴 사이에서 데이터를 전달하고 동기화하는 데 사용되는 통신 메커니즘이다. 채널을 통해 한 고루틴이 다른 고루틴에 안전하게 데이터를 보낼 수 있으며, 고루틴으로 실행중인 함수의 결과값이 준비될 때까지 대기하도록 할 수 있다.
func sendData(c chan<- int) {
for i := 1; i <= 5; i++ {
c <- i
}
close(c) // 채널을 닫아서 더 이상 데이터를 보낼 수 없음을 알림
}
func main() {
c := make(chan int)
go sendData(c)
for num := range c {
fmt.Println(num) // 채널에서 데이터를 계속 받다가 채널이 닫히면 반복문이 종료됨
}
fmt.Println("Finished receiving!")
}
- c <- thing 으로 채널에 thing이라는 데이터를 송신한다.
- sendData의 파라미터의 타입이 c chang<- int로 되어 있는데 이는 전달받은 채널을 송신전용으로 사용하겠다는 것이다.
- close(c)는 데이터를 더이상 송신할 수 없게 만드는 메소드이다.
- for num := range c를 통해 채널에 송신된 데이터들을 수신한다. 이는 블로킹 작업으로써 메인 메소드가 바로 종료되지 않게끔 한다.
VSCode에서 Go의 자동완성 및 자동줄맞춤을 위해서는 아래를 참조
https://code.visualstudio.com/docs/languages/go
Go with Visual Studio Code
Learn about Visual Studio Code editor features (code completion, debugging, snippets, linting) for Go.
code.visualstudio.com