Go를 처음 익히게 됐다.
당장 Go 관련 책을 사거나 영상을 보거나, 다른 사람의 글을 읽어도 되겠지만 우선은 가장 익숙한 Java랑 비교해서 아주 러프하게라도 흐름은 따라가보도록 해보기 위해 정리했다.
공부한다고 작성한 것이라 틀린 것이 있으면 학습하면서 수정해 나가려고 한다.
Go 언어를 Java 관점에서 이해하기 위한 비교 문서
| Go 개념 | Java 비교 | 설명 |
|---|---|---|
package | package | 동일 - 코드 그룹화 |
func | method | 함수 정의 |
struct | class | 데이터를 담는 구조체 (클래스와 유사하나 상속 없음) |
interface | interface | 동일 - 메서드 시그니처 정의 |
go.mod | pom.xml / build.gradle | 의존성 관리 파일 |
import | import | 동일 |
:= | 변수 선언 | var name = value의 축약형 |
ctx context.Context | ThreadLocal | 요청 컨텍스트 전달 |
| Java Spring | Go |
|---|---|
SpringApplication.run() | main.go: main() |
@Configuration 클래스 | config.go: LoadConfig() |
| Application 초기화 | app.go: NewApp() |
| Tomcat 서버 시작 | app.go: Run() |
| 계층 | Java Spring | Go eva-vsc |
|---|---|---|
| 진입점 | SpringApplication.run() | main.go |
| 설정 | @Configuration | config.go |
| 라우팅 | @RequestMapping | router.go |
| 필터 | Filter/Interceptor | middleware/ |
| 컨트롤러 | @Controller | handler/ |
| DTO | @RequestBody 클래스 | message/ |
| 모델 | Entity/VO | model/ |
| 서비스 | @Service | pkg/ 내 클라이언트들 |
try {
result = service.call();
} catch (Exception e) {
// 에러 처리
}
result, err := service.Call()
if err != nil {
// 에러 처리
return err
}
// 정상 처리
public class MyService implements Service {
// 메서드 구현
}
// 메서드만 구현하면 자동으로 인터페이스 충족
type PubSub interface {
Publish(subject string, data []byte) error
Subscribe(subject string, cb MsgHandler) error
}
// natsConnWrapper는 위 메서드들을 구현했으므로
// 자동으로 PubSub 인터페이스를 충족
type natsConnWrapper struct { ... }
func (n *natsConnWrapper) Publish(...) error { ... }
func (n *natsConnWrapper) Subscribe(...) error { ... }
new Thread(() -> doSomething()).start();
go doSomething() // 새 고루틴에서 비동기 실행
Go의 채널은 Java의 BlockingQueue와 유사하다.
ch := make(chan Message)
// 송신
go func() {
ch <- message
}()
// 수신
msg := <-ch
public void doFilter(req, res, chain) {
// 전처리
chain.doFilter(req, res);
// 후처리
}
func middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 전처리
next.ServeHTTP(w, r)
// 후처리
})
}
@RestController
@RequestMapping("/api")
public class ApiController {
@Autowired
private Service service;
@PostMapping("/events")
public ResponseEntity<Response> events(@RequestBody EventDTO dto) {
// 비즈니스 로직
return ResponseEntity.ok(result);
}
}
type Handler struct {
service Service
}
func (h *Handler) Events(w http.ResponseWriter, r *http.Request) {
var dto EventDTO
json.NewDecoder(r.Body).Decode(&dto)
// 비즈니스 로직
json.NewEncoder(w).Encode(result)
}
Go에서 func (a *App) Run()의 *는 포인터(Pointer)를 의미
// Java - 클래스 안에 메서드 정의
public class App {
public void run() {
this.doSomething(); // this로 자기 자신 참조
}
}
// Go 메소드 구조
func (m *AuthMiddleware) Handler(next http.Handler) http.Handler
// └──────┬───────┘ └──┬──┘ └───────┬───────┘ └─────┬─────┘
// 리시버 메소드명 파라미터 반환타입
// Go - 구조체 밖에서 메서드 정의
type App struct {
config Config
}
func (a *App) Run() {
a.doSomething() // a가 Java의 this 역할
}
* 유무의 차이| 문법 | 이름 | 의미 |
|---|---|---|
func (a App) | 값 리시버 | 복사본 전달 (원본 수정 불가) |
func (a *App) | 포인터 리시버 | 원본 참조 전달 (원본 수정 가능) |
type Counter struct {
count int
}
// 값 리시버 - 원본 변경 안 됨
func (c Counter) IncrementWrong() {
c.count++ // 복사본만 증가, 원본은 그대로
}
// 포인터 리시버 - 원본 변경됨
func (c *Counter) Increment() {
c.count++ // 원본이 증가
}
// 값 리시버 = 이런 느낌 (실제론 불가능)
void increment(Counter copy) { // 복사본 전달
copy.count++; // 원본 영향 없음
}
// 포인터 리시버 = 일반적인 Java 메서드
void increment() {
this.count++; // 원본 수정
}
Go에서 *가 붙은 포인터 리시버를 쓰는 이유:
1. 원본 수정이 필요할 때 (상태 변경)
2. 구조체가 클 때 (복사 비용 절약)
Java에서는 객체가 항상 참조로 전달되므로 이런 구분이 없지만, Go는 명시적으로 지정해야 한다. 대부분의 경우 *를 붙인 포인터 리시버를 사용
x := 10
p := &x // & : x의 주소를 얻어 p에 저장 (p는 *int)
*p = 20 // * : p가 가리키는 실제 값을 수정
// & 는 주소를 만들기
// * 는 포인터를 선언하거나, 포인터를 따라가 실제 값을 읽고, 쓰기
// 포인터는 필요한 시점에 한 번 따라가서 값 읽기/쓰기를 진행함
// **결과값 + 에러를 같이 반환**하는 이 패턴이 Go 코드 어디서나 보임
// Java의 try-catch 대신 이걸 쓰는 것
func findUser(id int) (User, error) {
user, err := db.Query(id)
if err != nil {
return User{}, err // 빈 User와 에러 반환
}
return user, nil // 유저와 nil(에러 없음) 반환
}
// 사용할 때
user, err := findUser(123)
if err != nil {
// 에러 처리
}
bool
string
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
byte // alias for uint8
rune // alias for int32
// represents a Unicode code point
float32 float64
complex64 complex128
Java의 BufferedReader와 개념적으로 유사 — I/O 횟수를 줄이기 위한 버퍼링이 핵심
Go bufio | Java BufferedReader | |
|---|---|---|
| 목적 | 버퍼를 두어 I/O 횟수 감소 | 동일 |
| 래핑 방식 | 기존 Reader를 감쌈 | 기존 Reader를 감쌈 |
| 줄 읽기 | ReadString('\n') | readLine() |
// Java
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
String line = reader.readLine();
// Go
reader := bufio.NewReader(os.Stdin)
line, err := reader.ReadString('\n') // '\n'까지 읽음
ReadString('\n')은 구분자 문자('\n')를 결과에 포함시킴readLine()은 구분자를 제외하고 반환strings.TrimSpace(line)으로 개행 문자를 직접 제거해야 함