1753번: 최단경로

첫째 줄에 정점의 개수 V와 간선의 개수 E가 주어진다. (1 ≤ V ≤ 20,000, 1 ≤ E ≤ 300,000) 모든 정점에는 1부터 V까지 번호가 매겨져 있다고 가정한다. 둘째 줄에는 시작 정점의 번호 K(1 ≤ K ≤ V)가

www.acmicpc.net

 


 

문제

정점 v <= 20,000과 간선 e <= 300,000개수가 주어진다.

시작 정점 s가 주어졌을 때, 시작정점에서부터 각 정점까지 최단거리를 출력하자. 단, 시작정점부터 못 가는 정점은 INF를 출력하자.

 

 

풀이

각 정점마다 가장 빠른 거리만 최신화하며 탐색하면 된다고 판단했다. -> 다익스트라

정점의 개수만큼 distance라는 int형 배열을 만들고, 간선 w의 최대값인 10과 정점의 최대값인 20,000을 곱한 200,000보다 1큰  200,001로 초기화했다. 그래프를 탐색하며 distance 배열을 최신화시킬 수 있는 탐색만 진행했다.

 

 

코드

import java.io.*;
import java.math.BigInteger;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.IntStream;

public class Main {
    static int v,e,s;
    static List<Node>[] list;
    static int[] distance;

    public static void main(String[] args) throws Exception {
        InputReader in = new InputReader(System.in);

        v = in.nextInt();
        e = in.nextInt();
        s = in.nextInt();
        list = new ArrayList[v+1];
        distance = new int[v+1];
        for(int i = 1; i <= v; i++) {
            list[i] = new ArrayList<Node>();
            distance[i] = 200001;
        }

        for(int i = 0; i < e; i++) {
            int s = in.nextInt();
            int e = in.nextInt();
            int w = in.nextInt();

            list[s].add(new Node(e, w));
        }

        bfs();
        StringBuilder sb = new StringBuilder();
        for(int i = 1; i <= v; i++) {
            if(i==s) sb.append("0");
            else if(distance[i] != 200001) sb.append(distance[i]);
            else sb.append("INF");
            sb.append("\n");
        }

        System.out.println(sb);
    }

    public static void bfs() {
        PriorityQueue<Node> pq = new PriorityQueue<>();
        pq.add(new Node(s, 0));
        distance[s] = 0;
        while(!pq.isEmpty()) {
            Node node = pq.poll();
            for(int i = 0; i < list[node.next].size(); i++) {
                Node next = list[node.next].get(i);

                if(distance[next.next] <= distance[node.next] + next.distance) continue;

                distance[next.next] = distance[node.next] + next.distance;
                pq.add(new Node(next.next, node.distance+next.distance));
            }
        }
    }
}

class Node implements Comparable<Node> {
    int next;
    int distance;

    public Node(int next, int distance) {
        this.next = next;
        this.distance = distance;
    }

    @Override
    public int compareTo(Node o) {
        return this.distance - o.distance;
    }
}

class InputReader {
    private final InputStream stream;
    private final byte[] buf = new byte[8192];
    private int curChar, snumChars;

    public InputReader(InputStream st) {
        this.stream = st;
    }

    public int read() {
        if (snumChars == -1)
            throw new InputMismatchException();
        if (curChar >= snumChars) {
            curChar = 0;
            try {
                snumChars = stream.read(buf);
            } catch (IOException e) {
                throw new InputMismatchException();
            }
            if (snumChars <= 0)
                return -1;
        }
        return buf[curChar++];
    }

    public int nextInt() {
        int c = read();
        while (isSpaceChar(c)) {
            c = read();
        }
        int sgn = 1;
        if (c == '-') {
            sgn = -1;
            c = read();
        }
        int res = 0;
        do {
            res *= 10;
            res += c - '0';
            c = read();
        } while (!isSpaceChar(c));
        return res * sgn;
    }

    public long nextLong() {
        int c = read();
        while (isSpaceChar(c)) {
            c = read();
        }
        int sgn = 1;
        if (c == '-') {
            sgn = -1;
            c = read();
        }
        long res = 0;
        do {
            res *= 10;
            res += c - '0';
            c = read();
        } while (!isSpaceChar(c));
        return res * sgn;
    }

    public int[] nextIntArray(int n) {
        int a[] = new int[n];
        for (int i = 0; i < n; i++) {
            a[i] = nextInt();
        }
        return a;
    }

    public String nextLine() {
        int c = read();
        while (isSpaceChar(c))
            c = read();
        StringBuilder res = new StringBuilder();
        do {
            res.appendCodePoint(c);
            c = read();
        } while (!isEndOfLine(c));
        return res.toString();
    }

    public boolean isSpaceChar(int c) {
        return c == ' ' || c == '\n' || c == '\r' || c == '\t' || c == -1;
    }

    private boolean isEndOfLine(int c) {
        return c == '\n' || c == '\r' || c == -1;
    }
}

 

 

5052번: 전화번호 목록

첫째 줄에 테스트 케이스의 개수 t가 주어진다. (1 ≤ t ≤ 50) 각 테스트 케이스의 첫째 줄에는 전화번호의 수 n이 주어진다. (1 ≤ n ≤ 10000) 다음 n개의 줄에는 목록에 포함되어 있는 전화번호가

www.acmicpc.net

 

 


 

설명

전화번호 목록이 있다. 해당 목록의 일관성을 확인해라.

전화번호 목록의 일관성은 한 번호가 다른 번호의 접두어이면 안된다는 것이다.

e.g. 119, 11923

 

풀이

해당 문제의 경우 전화번호 수인 N이 최대 10만이기 때문에, O(N^2)의 경우 시간내에 들어오지 못한다. O(NlogN) 방법을 생각했고, 같은 접두어인 경우 인접해있기 때문에 정렬을 사용했다.

 

코드

public class Main {

    public static void main(String[] args) throws Exception {
        InputReader in = new InputReader(System.in);

        int testcase = in.nextInt();
        StringBuilder sb = new StringBuilder();
        while(testcase-->0) {
            int N = in.nextInt();
            String[] numberOfPhone = new String[N];

            for(int i = 0; i < N; i++) {
                numberOfPhone[i] = in.nextLine();
            }

            Arrays.sort(numberOfPhone);

            boolean flag = false;
            for(int i = 1; i < N; i++) {
                if(numberOfPhone[i].startsWith(numberOfPhone[i-1])) {
                    flag = true;
                    break;
                }
            }

            sb.append((flag) ? "NO\n" : "YES\n");
        }

        System.out.println(sb);
    }
}

 

 

 

2467번: 용액

첫째 줄에는 전체 용액의 수 N이 입력된다. N은 2 이상 100,000 이하의 정수이다. 둘째 줄에는 용액의 특성값을 나타내는 N개의 정수가 빈칸을 사이에 두고 오름차순으로 입력되며, 이 수들은 모두 -

www.acmicpc.net

2020.10.20 코드

 

  • 생각

투 포인터 방식을 사용하면 될 것 같다.

1. 일단 입력 받은 배열을 오름차순 정렬을 통해 왼쪽부터 오른쪽으로 점점 수가 커지게 정렬시킨다.

2. 용액 배열 양쪽에서 다가오면서, 두 개의 합이 0에 가장 가까울 경우 저장해준다.

3. 두 개의 합이 0 보다 작을 경우, 조금 더 0보다 가까운 경우가 있는지 알아보기 위해 왼쪽에서 다가오는 index를 하나 늘려준다.

4. 두 개의 합이 0보다 클 경우, 조금 더 0보다 가까운 경우가 있는지 알아보기 위해 오른쪽에서 다가오는 index를 하나 줄여준다.

 

  • 코드

 

import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.InputMismatchException;

public class Main {
    static int[] array;
    static int N, answer1, answer2;
	
	public static void main(String[] args) throws Exception {
		SetData();
		TwoPointer();
		System.out.println(answer1 + " " + answer2);
	}

	// 데이터
	private static void SetData() throws Exception {
		InputReader in = new InputReader(System.in);
		
        N = in.nextInt();
		array = new int[N];
		answer1 = answer2 = 0;

		for (int i = 0; i < N; ++i) {
			array[i] = in.nextInt();
		}
		Arrays.sort(array);
	}

	private static void TwoPointer() {
		int min = Integer.MAX_VALUE;
		int left = 0, right = N - 1;
		
		// 투 포인터
		while (left < right) {
			int sum = array[left] + array[right];

			// v가 최소일 때 특성값 갱신
			if (min > Math.abs(sum)) {
				min = Math.abs(sum);
				answer1 = array[left];
				answer2 = array[right];
			}

			if (sum > 0)
				right--;
			else
				left++;
		}
	}
}

class InputReader {

	private final InputStream stream;
	private final byte[] buf = new byte[8192];
	private int curChar, snumChars;

	public InputReader(InputStream st) {
		this.stream = st;
	}

	public int read() {
		if (snumChars == -1)
			throw new InputMismatchException();
		if (curChar >= snumChars) {
			curChar = 0;
			try {
				snumChars = stream.read(buf);
			} catch (IOException e) {
				throw new InputMismatchException();
			}
			if (snumChars <= 0)
				return -1;
		}
		return buf[curChar++];
	}

	public int nextInt() {
		int c = read();
		while (isSpaceChar(c)) {
			c = read();
		}
		int sgn = 1;
		if (c == '-') {
			sgn = -1;
			c = read();
		}
		int res = 0;
		do {
			res *= 10;
			res += c - '0';
			c = read();
		} while (!isSpaceChar(c));
		return res * sgn;
	}

	public long nextLong() {
		int c = read();
		while (isSpaceChar(c)) {
			c = read();
		}
		int sgn = 1;
		if (c == '-') {
			sgn = -1;
			c = read();
		}
		long res = 0;
		do {
			res *= 10;
			res += c - '0';
			c = read();
		} while (!isSpaceChar(c));
		return res * sgn;
	}

	public int[] nextIntArray(int n) {
		int a[] = new int[n];
		for (int i = 0; i < n; i++) {
			a[i] = nextInt();
		}
		return a;
	}

	public String readString() {
		int c = read();
		while (isSpaceChar(c)) {
			c = read();
		}
		StringBuilder res = new StringBuilder();
		do {
			res.appendCodePoint(c);
			c = read();
		} while (!isSpaceChar(c));
		return res.toString();
	}

	public String nextLine() {
		int c = read();
		while (isSpaceChar(c))
			c = read();
		StringBuilder res = new StringBuilder();
		do {
			res.appendCodePoint(c);
			c = read();
		} while (!isEndOfLine(c));
		return res.toString();
	}

	public boolean isSpaceChar(int c) {
		return c == ' ' || c == '\n' || c == '\r' || c == '\t' || c == -1;
	}

	private boolean isEndOfLine(int c) {
		return c == '\n' || c == '\r' || c == -1;
	}
}

 

 

Retry

2024.1.15 코드

풀이

일단, N개의 입력은 정렬된 상태로 입력이 된다고 주어진다. (-> 굳이 정렬을 할 필요가 없다는 뜻)

 

두 수의 합이 0에 가장 가까운 방법을 찾으면 되기때문에 가장 왼쪽과 가장 오른쪽을 더 해주면서 절대값이 더 큰 값의 index를 왼쪽은 + 1, 오른쪽은 - 1하며 반복하면 된다고 생각했다. 왼쪽 index가 오른쪽 index보다 작을 때까지만!

 

    public static void main(String[] args) throws Exception {
        InputReader in = new InputReader(System.in);

        int n = in.nextInt();
        int[] arr = new int[n];
        for(int i = 0; i < n; i++) arr[i] = in.nextInt();

        int minimum = 100;
        int al=0, san=0;
        int left = 0, right = n-1;
        while(left < right) {
            int sum = arr[left] + arr[right];
            // 합이 이전 비교보다 0에 가까울 경우
            if(Math.abs(sum) <= minimum) {
                al = arr[left];
                san = arr[right];
                minimum = Math.abs(sum);
            }

            if(Math.abs(arr[left]) <= Math.abs(arr[right])) {
                right--;
            } else {
                left++;
            }
        }
        System.out.println(al + " " + san);
    }

첫 풀이는 위와 같다. 하지만, 27% 정도에서 틀렸습니다.

 

문제를 다시 잘 읽어본 결과 minimum 초기 값이 너무 작아서 틀렸다고 나온 것 같았다. 10억 2개만 있는 경우 더하면 20억이 될 수도 있기때문이다.

    public static void main(String[] args) throws Exception {
        InputReader in = new InputReader(System.in);

        int n = in.nextInt();
        int[] arr = new int[n];
        for(int i = 0; i < n; i++) arr[i] = in.nextInt();

        int minimum = 2000000001;        // 20억보다 1큰 20억 1로 초기화
        int al=0, san=0;
        int left = 0, right = n-1;
        while(left < right) {
            int sum = arr[left] + arr[right];
            // 합이 이전 비교보다 0에 가까울 경우
            if(Math.abs(sum) <= minimum) {
                al = arr[left];
                san = arr[right];
                minimum = Math.abs(sum);
            }

            if(Math.abs(arr[left]) <= Math.abs(arr[right])) {
                right--;
            } else {
                left++;
            }
        }
        System.out.println(al + " " + san);
    }

위의 코드로 Success!

 

Go 풀이

package main

import (
	"bufio"
	"fmt"
	"math"
	"os"
)

var reader *bufio.Reader = bufio.NewReader(os.Stdin)
var writer *bufio.Writer = bufio.NewWriter(os.Stdout)

func main() {
	defer writer.Flush()
	var n int
	fmt.Fscan(reader, &n)
	arr := make([]int, n)
	for i := 0; i < n; i++{
		fmt.Fscan(reader, &arr[i])
	}

	minimum := 2000000001
	al := 0
	san := 0
	left := 0
	right := n-1
	for left < right {
		sum := arr[left] + arr[right]
		if(int(math.Abs(float64(sum))) <= minimum) {
			al = arr[left];
			san = arr[right];
			minimum = int(math.Abs(float64(sum)));
		}

		if(int(math.Abs(float64(arr[left]))) <= int(math.Abs(float64(arr[right])))) {
			right--;
		} else {
			left++;
		}
	}
	fmt.Fprintln(writer, al, san)
}

기존 fmt.Scanf혹은 Println을 사용하면 시간 초과가 뜬다. 더 빠르게 입출력을 할 수 있도록 도와주는 라이브러리를 사용해야 한다고 함.

bufio라는 라이브러이고 링크를 보면 도움이 될 것이다.

Go Routine을 사용하지 않은 URL Checker 

지금까지 배운 Go 사용법으로 URL Checker를 만들어 보려고한다.

package main

import (
	"errors"
	"net/http"
)

var errRequestFailed = errors.New("Request is failed")

func main() {
	urls := []string{
		"https://www.naver.com/",
		"https://www.google.com/",
		"https://www.amazon.com/",
		"https://www.facebook.com/",
		"https://www.instagram.com/",
		"https://www.naver.com/",
	}
	for _, url := range urls {
		fmt.Println(hitURL(url))
	}
}

func hitURL(url string) error {
	resp, err := http.Get(url)
	if err != nil || resp.StatusCode >= 400 {
		return errRequestFailed
	}
	return nil
}

main.go

일단 여러 url들을 배열안에 넣어두고 for문을 통해 url이 정상적인 응답을 주는지 확인해보았다. http의 경우 Go에서 기본적으로 지원하는 라이브러리를 사용했다. 링크를 통해 사용법을 배웠다.

resp, err := http.Get("http://example.com/")
if err != nil {
	// handle error
}

짤막하게 설명하자면 http method 중 하나인 Get을 사용하기 위해서는 http.Get 메소드에 url을 넣어서 요청하면 response와 error를 반환한다. error를 받은 경우 개발자가 스스로 error handling을 할 수 있도록 구현해두었다.

 

위의 사용법을 기반으로 아래와 같은 hitURL이라는 함수를 작성했다.

func hitURL(url string) error {
	resp, err := http.Get(url)
	if err != nil || resp.StatusCode >= 400 {
		return errRequestFailed
	}
	return nil
}

url에 요청을 보내고 받은 response와 error를 받아오고 err가 발생했거나 응답의 상태코드가 400이상이면 request가 실패했다는 error를 return하고 아닐 시 nil을 return 하도록 했다.

 

프로젝트를 돌려보면 6개의 응답 모두 nil을 return하여 문제없는 url임을 확인할 수 있다. 조금 더 결과물을 보기 좋게하기위해 map을 사용해보자.

package main

import (
	"errors"
	"fmt"
	"net/http"
)

var errRequestFailed = errors.New("Request is failed")

func main() {
	results := map[string]string{}			// ADD
	urls := []string{
		"https://www.naver.com/",
		"https://www.google.com/",
		"https://www.amazon.com/",
		"https://www.facebook.com/",
		"https://www.instagram.com/",
		"https://www.naver.com/",
		"https://www.airbnb.com/",
		"https://www.reddit.com/",
	}
	for _, url := range urls {
		result := "OK"			// ADD
		err := hitURL(url)
		if err != nil {			// ADD
			result = "FAILED"			// ADD
		}			// ADD
		results[url] = result			// ADD
	}
	// ADD
	for url, result := range results {
		fmt.Println(url, result)
	}
}

func hitURL(url string) error {
	fmt.Println("Checking: ", url)			// ADD
	resp, err := http.Get(url)
	if err != nil || resp.StatusCode >= 400 {
		return errRequestFailed
	}
	return nil
}

출력

string 배열에있는 모든 url을 확인한 뒤, 결과물인 성공, 실패 여부를 map에 저장하여 출력해 준 모습이다. 문제없는 url만 저장해서인지 FAILED가 하나도 없다.

 

하지만, 이러한 URL Checker를 만드는 데에는 다른 언어와의 큰 차이점을 느낄 수 없다. 현재는 모든 URL을 하나하나하나 Checking하며, 이전 작업이 완료되어야만 다음 작업을 수행하고 있다. Go에는 이러한 작업을 한 번에 수행할 수 있도록 해주는 Go Routine이라는 기술이 있는데, 현재 사용해보면 좋을 것 같아서 적용해보려고 한다.

tip!

go에서는 make()라는 함수가 있다.
// The make built-in function allocates and initializes an object of type
// slice, map, or chan (only). Like new, the first argument is a type, not a
// value. Unlike new, make's return type is the same as the type of its
// argument, not a pointer to it. The specification of the result depends on
// the type:
//
//	Slice: The size specifies the length. The capacity of the slice is
//	equal to its length. A second integer argument may be provided to
//	specify a different capacity; it must be no smaller than the
//	length. For example, make([]int, 0, 10) allocates an underlying array
//	of size 10 and returns a slice of length 0 and capacity 10 that is
//	backed by this underlying array.
//	Map: An empty map is allocated with enough space to hold the
//	specified number of elements. The size may be omitted, in which case
//	a small starting size is allocated.
//	Channel: The channel's buffer is initialized with the specified
//	buffer capacity. If zero, or the size is omitted, the channel is
//	unbuffered.
func make(t Type, size ...IntegerType) Type

command+make()라는 함수를 클릭하면 위와같은 builtin.go라는 파일에 코드가 생성되어 있다. go에서 제공해주는 함수인데, make()라는 함수는 주석을 정리해보면 slice, map, chan과 같은 타입의 object를 할당 및 초기화 해준다고 설명되어 있다.  사용법도 각각의 object 별로 주석으로 설명이 잘 되어있기 때문에 읽어보고 사용해봐도 좋다.

 

위 map을 make()라는 함수를 통해 만들어보자.

// Map: An empty map is allocated with enough space to hold the
// specified number of elements. The size may be omitted, in which case
// a small starting size is allocated.
results := map[string]string{}

기존 key와 value가 string인 map을 생성할 때 위와같이 작생했다.

results := make(map[string]string)

make를 사용하면 위와같은 코드로 만들 수 있다.

 

음.... map을 생성할 때는 make를 굳이 사용할 필요가 있을까? 라는 의문이 든다. {} -> make() 의 차이점은 오히려 코드를 많이 적게 만드는 것이 아닌가?라는 의문이 들지만, 가독성을 생각한다면 사용할만한 가치가 있다고 생각한다. 코드상의 큰 차이점은 없지만, make라는 의미를 부여하기 때문이다. map[string]string이라는 object를 생성한다는 것을 보다 직관적으로 알 수 있기 때문이다.

results := make(map[string]string)
results := map[string]string{}

위 코드를 보며, 개인적으로 보다 나은 방식을 선택하면 좋을 것 같다.


Go Routine

다른 함수와 동시에 실행시키는 함수

Go에서 최적화하는 방법은 동시에 작업을 처리하는 것이다. Go에서는 이러한 동시 작업 처리를 위해서는 Go routine이라는 컨셉을 이해해야 한다. 

func main() {
	testCount("test1")
	testCount("test2")
}

func testCount(person string) {
	for i := 0; i < 10; i++ {
		fmt.Println(person, "is test", i)
		time.Sleep(time.Second)
	}
}

main.go

테스트를 보다 쉽게하기 위해 조금 더 이해해보기 쉽게 main.go를 위와같이 작성했다.

출력

순서대로 출력되는 모습을 확인할 수 있다. Go routine을 한 번 적용해보자. Go routine을 적용하는 방법은 아주 간단하다. 함수를 불러오는 코드에 함수 앞에 go만 붙이면 된다. 예를들어, testCount()를 go testCount()로 작성하면 된다.

func main() {
	go testCount("test1")
	testCount("test2")
}

func testCount(person string) {
	for i := 0; i < 10; i++ {
		fmt.Println(person, "is test", i)
		time.Sleep(time.Second)
	}
}

출력

test1, test2가 같이실행되는 것을 볼 수 있다. 그렇다면, 두 함수 모두 Go routine으로 처리하면 어떻게 될까?

func main() {
	go testCount("test1")
	go testCount("test2")
}

func testCount(person string) {
	for i := 0; i < 10; i++ {
		fmt.Println(person, "is test", i)
		time.Sleep(time.Second)
	}
}

코드를 위와같이 수정하고 돌려지만, 출력되는 결과물은 없었다. 그 이유는 main()에 작성된 두 함수에 접근하며 아래작성 된 코드를 실행하는데, testCount("test1")과 testCount("test2")를 돌리는 중간에 main() 함수가 종료되었기 때문이다. Go routine은 프로그램이 작동하는 동안만 유효하기 때문에 이 점을 유념해서 코드를 작성해야 한다.

 

그렇다면, go routine을 쓰면 해당 작업이 끝나는 시간을 고려해서 이후 코드들을 작성해야하는가..?  google 개발자들이 이렇게 허술하게 만들었을리가 없다고 생각했다면 역시나 이를 고려해서 channel이라는 기능을 만들었다. 

 

Channel

goroutine 간 통신을 위한 메커니즘으로, 데이터를 안전하게 전달하고 동기화하는 데 사용

ChatGPT에 의하면 Channel은 아래와 같은 기능을 위해 존재한다고 말한다.

  1. 동시성과 통신: channel은 고루틴 간의 통신을 위해 디자인되었습니다. 여러 고루틴이 병렬로 실행되는 환경에서 안전하게 데이터를 주고받을 수 있도록 합니다.
  2. 데이터 전달: channel을 사용하여 데이터를 안전하게 전달할 수 있습니다. 데이터를 보낼 때는 <- 연산자를 사용하며, 데이터를 받을 때는 <- 연산자를 반대로 사용합니다.
  3. 동기화: channel은 데이터를 전달할 때 발신자와 수신자 간에 동기화를 제공합니다. 즉, 데이터를 주고 받는 과정에서 발신자가 데이터를 보낼 때까지 대기하게 되며, 수신자는 데이터를 받을 때까지 대기하게 됩니다.

여기서 우리가 봐야할 건, 동기화에 있는 수신자는 데이터를 받을 때까지 대기하게 됩니다.이다. 즉, main에서 channel을 생성하여 goroutine을 사용하면, 수신자인 main에서 데이터를 받을 때까지 대기한다는 말이다.

 

실제로 적용해보자.

func main() {
	c := make(chan string)
	people := [2]string{"test1", "test2"}
	for _, person := range people {
		go testCount(person, c)
	}
	result := <- c
	fmt.Println(result)
}

func testCount(person string, c chan string) {
	c <- person
}

출력

string으로 channel을 생성하고 testCount()에 포함하여 보내주고, channel로 person이라는 string 데이터를 보냈다. 그 후 channel에서 받은 데이터를 result에 넣어주고 출력해주었다. 이전 아무런 출력이 일어나지 않은 것과는 다르게 test2라는 string이 출력된 모습을 볼 수 있다. test1, test2 두개의 string을 보냈는데 test2만 출력되는 이유는 channel은 queue나 pipe와 비슷한 형태로 1개씩 데이터를 보내고 받을 수 있기 때문이다. 

func main() {
	c := make(chan string)
	people := [2]string{"test1", "test2"}
	for _, person := range people {
		go testCount(person, c)
	}
	fmt.Println(<- c)			// 데이터 꺼내기
	fmt.Println(<- c)			// 데이터 꺼내기
}

func testCount(person string, c chan string) {
	c <- person
}

출력

위와같이 두 번 데이터를 꺼내면 전달한 두 개의 데이터를 모두 받을 수 있다.

그렇다면, 보낸 데이터가 없을 때 꺼낸다면 어떻게 될까?

func main() {
	c := make(chan string)
	people := [2]string{"test1", "test2"}
	for _, person := range people {
		go testCount(person, c)
	}
	fmt.Println(<- c)			// 있는 데이터 꺼내기
	fmt.Println(<- c)			// 있는 데이터 꺼내기
	fmt.Println(<- c)			// 없는 데이터 꺼내기
}

func testCount(person string, c chan string) {
	c <- person
}

출력

2개의 데이터를 꺼내고 교착상태가 일어난 모습을 볼 수 있다. 모든 goroutines은 모두 종료되어 sleep상태가 되었지만, main에서 데이터를 기다리고 있기때문에 deadlock이 발생한 상황이다. 

 

이러한 문제를 막기 위해서는 다양한 방법이 존재한다. 그 중 하나는 sync.WaitGroup을 사용해서 막는 것이다. goroutine이 종료되면 channel을 닫아주면 된다.

func main() {
	c := make(chan string)
	people := [2]string{"test1", "test2"}
	var wg sync.WaitGroup

	for _, person := range people {
		wg.Add(1)
		go testCount(person, c, &wg)
	}

	go func() {
		wg.Wait()
		close(c)
	}()

	for msg := range c {
		fmt.Println(msg)
	}
}

func testCount(person string, c chan string, wg *sync.WaitGroup) {
	defer wg.Done()
	c <- person
}

goroutine을 1개 부를 때마다 wg.Add()를 추가하고 goroutine이 종료되면 wg.Done()으로 추가한 것을 하나씩 제거한다. wg.Wait()으로 모든 goroutine이 종료됨을 확인하고 다 종료되었다면 close(c)를 통해 channel을 닫아준다. 그 후 channel의 범위만큼 반복문을 돌린다면 이전과 같은 상황을 막을 수 있다.

 

아니면 goroutine을 사용하는 수만큼 반복문을 돌려도 된다.

func main() {
	c := make(chan string)
	people := [2]string{"test1", "test2"}

	for _, person := range people {
		go testCount(person, c)
	}

	for i := 0; i < len(people); i++{
		fmt.Println(<-c)
	}
}

func testCount(person string, c chan string) {
	c <- person
}

people의 배열 크기만큼 goroutine의 수를 늘리기 때문에 배열 크기만큼 데이터를 받아오는 것이다. 그렇다면, 이전 URL Checker에 Go Routine을 적용해보자. 

 

 

URL Checker + Go Routine

type requestResult struct {
	url string
	status string			// OK, FAILED
}

먼저, channel로 전달할  url과 성공, 실패 여부 필드를 담은 struct를 추가했다.

c := make(chan requestResult)

만든 struct type의 chan을 생성했다.

	for _, url := range urls {
		go hitURL(url, c)
	}

main에서 hitURL을 goroutine으로 부르고

func hitURL(url string, c chan requestResult) {
	fmt.Println("Checking: ", url)
	resp, err := http.Get(url)
	status := "OK"
	if err != nil || resp.StatusCode >= 400 {
		status = "FAILED"
	}
	c <- requestResult{url: url, status: status}
}

hitURL에서 channel에 성공 여부를 struct에 담아서 보냈다.

	for i := 0; i < len(urls); i++ {
		fmt.Println(<-c)
	}

main에서 channel을 통해 받은 result struct를 출력했다.

출력

package main

import (
	"errors"
	"fmt"
	"net/http"
)

type requestResult struct {
	url string
	status string			// OK, FAILED
} 

var errRequestFailed = errors.New("Request is failed")

func main() {
	c := make(chan requestResult)
	urls := []string{
		"https://www.naver.com/",
		"https://www.google.com/",
		"https://www.amazon.com/",
		"https://www.facebook.com/",
		"https://www.instagram.com/",
		"https://www.naver.com/",
		"https://www.airbnb.com/",
		"https://www.reddit.com/",
	}
	for _, url := range urls {
		go hitURL(url, c)
	}

	for i := 0; i < len(urls); i++ {
		fmt.Println(<-c)
	}
}

func hitURL(url string, c chan requestResult) {
	fmt.Println("Checking: ", url)
	resp, err := http.Get(url)
	status := "OK"
	if err != nil || resp.StatusCode >= 400 {
		status = "FAILED"
	}
	c <- requestResult{url: url, status: status}
}

전체코드이다. goroutine을 사용하지 않았을 때와 비교가 크게 될 정도로 빠르게 종료되는 모습을 볼 수 있을 것이다. 병렬적으로 수행하기 때문. 정말 편리하게 병렬적으로 함수를 실행할 수 있는 것이 Go의 엄청 큰 장점이지 않을까 싶다.

 

 

 

'Go' 카테고리의 다른 글

Job Scrapper  (0) 2024.01.29
method를 활용한 map 구현(Search, Add, Delete, Update)  (0) 2024.01.10
struct/public,private/String()/error handling  (0) 2024.01.07
Struct  (0) 2024.01.04
Maps  (0) 2023.12.30

Dictionary.go

package mydict

// Dictionary type
type Dictionary map[string]string

위와 같은 type으로 된 Dictionary map을 만들어준다. 

패키지는 위와 같다.

main.go

func main() {
	dictionary := mydict.Dictionary{"first":"first"}
	fmt.Println(dictionary)
}

main에서 위와같이 사용할 수 있을 것이다. 하지만, 에러 처리등 불편한 점이 존재하고있는 상태다. 이전에 배운 method를 활용해 main을 가독성 좋게 작성해보자.

 

Search

var errNotFound = errors.New("Not Found")

func(d Dictionary) Search(word string) (string, error) {
	value, exists := d[word]
	if exists {
		return value, nil
	}
	return "", errNotFound
}

mydict.go에 위 method를 추가한다.

func main() {
	dictionary := mydict.Dictionary{"first":"first"}
	value, err := dictionary.Search("second")
	if err != nil {
		fmt.Println(err)
	} else {
		fmt.Println(value)
	}
}

main.go에서 dictionary에 있는 search method를 사용해 출려본다.

출력

정상적으로 error handling이 되는 모습이다. err가 발생했을 때, 시스템이 더이상 돌아가지 않게 하고싶다면 다른 방법으로 if문 안을 처리하면 된다. 이러한 방식이 단순히 map을 사용하여 key를 통해 value를 얻는 방식보다 개발자가 원하는 방식대로 error를 handling할 수 있기때문에 각자의 취향대로 선택하면 될 것 같다.

 

 

Add

var errExistsWord = errors.New("That word already exists")

func(d Dictionary) Add(word, def string) error {
	_, err := d.Search(word)	// 이미 key로 된 중복된 단어가 있는지 확인
	switch err {
	case errNotFound:
		d[word] = def
	case nil:
		return errExistsWord
	}
	return nil
}

mydict.go에 위와같은 method를 추가해준다.

func main() {
	dictionary := mydict.Dictionary{}
	err := dictionary.Add("first", "first")
	if err != nil {
		fmt.Println(err)
	}
	def, err := dictionary.Search("first")
	if err != nil {
		fmt.Println(err)
	} else {
		fmt.Println(def)
	}
}

main.go에서 확인해주면 정상적으로 추가가되고, error가 handling되는 모습을 볼 수 있다.

 

 

Update Delete

var errCantUpdate = errors.New("Can`t update non-exitstiong word")

func(d Dictionary) Update(word, def string) error {
	_, err := d.Search(word)
	switch err {
	case nil:
		d[word] = def
	case errNotFound:
		return errExistsWord
	}
	return nil
}

// Delete a word
func(d Dictionary) Delete(word string) {
	delete(d, word)
	// word가 없다면 아무 작업도 하지 않음
}

mydict.go 에 위와 같은 메소드를 추가해준다.

func main() {
	dictionary := mydict.Dictionary{}
	err1 := dictionary.Add("first", "first")
	if err1 != nil {
		fmt.Println(err1)
	}
	
	err2 := dictionary.Update("first", "second")
	if err2 != nil {
		fmt.Println(err2)
	}
	word, _ := dictionary.Search("first")
	fmt.Println(word)

	dictionary.Delete("first")
	word2,err3 := dictionary.Search("first")
	if err3 != nil {
		fmt.Println(err3)
	} else {
		fmt.Println(word2)
	}
}

출력

map의 key가 "first"인 value의 값이 "second"로 변경되어 출력되었고, delete후 search를 했을 때 찾을 수 없다는 error handling이 된 것을 확인할 수 있다.

 

'Go' 카테고리의 다른 글

Job Scrapper  (0) 2024.01.29
URL Checker & Go Routine  (0) 2024.01.14
struct/public,private/String()/error handling  (0) 2024.01.07
Struct  (0) 2024.01.04
Maps  (0) 2023.12.30

Struct

다양한 struct를 만들며 struct에 대한 학습을 하려함

Backing.go 라는 파일을 만들고, 아래와 같은 struct를 만들어서 main.go에서 Import해서 사용해보려한다. 

package banking

// Account Struct
type Account struct {	
	owner string
	balance int
}

export를 하기 위해 struct의 시작 문자는 대문자로 해주어야 한다.

package main

import (
	"github.com/qazyj/learngo/banking"
)

func main() {
	account := banking.Account{owner:"qazyj", balance: 1000}
}

파일 구조

하지만, field 값을 제대로 못 읽어 오는 듯 하다.

 

이유는 Account가 private이기 때문이다. ?? Java에서 public, private를 선언해주는 것도 아닌데 왜 private인지 의문이 들 것이다. Go에서는 public과 private가 소문자, 대문자로 시작하는 것을 의미한다.

public : 대문자 시작
private : 소문자 시작

 

이처럼 field 변수명도 대문자로 시작해줘야지만 외부 파일에서 사용할 수 있다.

package banking

// Account Struct
type Account struct {	
	Owner string			// 대문자 시작
	Balance int				// 대문자 시작
}

Banking.go

package main

import (
	"github.com/qazyj/learngo/banking"
)

func main() {
	account := banking.Account{Owner:"qazyj", Balance: 1000}
}

Main.go

 

import

대문자 시작으로 바꿔주면 정상적으로 import가 되어 사용이 가능하다. 

 

Java에서는 public, private, protected를 통해 외부 클래스 파일의 접근을 허용한다. 즉, 외부의 접근을 신경쓰기 위해 public, private, protected 중 하나를 추가해야된다는 뜻이다. 하지만, Go에서는 시작 문자를 소/대문자를 통해 public과 private를 구분한다. 코드가 더 간결해지고 한 눈에 알아볼 수 있다는 장점이 있다. 

 

정리

package banking

// Account Struct
type Account struct {	
	owner string			// private
	Balance int				// public
}

 

 

하지만, 나는 Owner라는 이름은 Account를 생성할 때 받고싶지만 Balance는 0이라는 고정값으로 설정하고 싶다. 

func main() {
	account := banking.Account{Owner:"qazyj"}
	fmt.Println(account)
}

출력

Go에서는 struct에 대한 다양한 생성자를 자동으로 만들어주는 듯 하다. struct를 생성할 때 설정되지 않은 필드값은 자동으로 default값으로 설정될 수 있도록 말이다. 위와같이 code를 변경하면, Balance라는 필드값은 private인 balance로 변경해도 문제없이 작동한다.

 

struct의 필드값을 public으로 변경하면 아래와 같은 작업도 가능하다.

func main() {
	account := banking.Account{Owner:"qazyj"}
	account.Owner = "test"
	fmt.Println(account)
}

출력

필드값 변경이 용이하다는 장점이 있지만, 필드값 변경이 무분별하게 진행될 수 있다는 단점도 존재한다. 이러한 단점을 막기위해서는 function을 만들어주어야 한다. 

type Account struct {	
	owner string
	balance int
}

func NewAccount(owner string) *Account{
	account := Account{
		owner: owner,
		balance: 0,
	}
	return &account
}

많은 예제 코드들을 확인해본 결과 생성자에는 New+struct 이름으로 만드는 듯 하다.

func main() {
	account := accounts.NewAccount("qazyj")
	fmt.Println(account)
}

출력

실제 메모리 address를 return하여 복사본이 아닌 만든 값을 return 해주면 된다. account라는 struct를 출력할 때, field값들이 출력되는 이유는 struct를 출력할 때 내부적으로 String 메소드를 호출하여 출력하기 때문이다. String 메소드를 정의하지 않은 경우 Go에서 자동으로 기본 field 값을 포맷팅하여 return 해준다. 링크를 통해 확인해보니 printValue라는 메소드를 통해 아래와 같이 포인트인지 확인한다.

	case reflect.Pointer:
		// pointer to array or slice or struct? ok at top level
		// but not embedded (avoid loops)
		if depth == 0 && f.UnsafePointer() != nil {
			switch a := f.Elem(); a.Kind() {
			case reflect.Array, reflect.Slice, reflect.Struct, reflect.Map:
				p.buf.writeByte('&')
				p.printValue(a, verb, depth+1)
				return
			}
		}
		fallthrough

그 후 다시 printValue로 들어가 아래와 struct인지 확인해주고 출력될 string을 추가로 만들어준다.

	case reflect.Struct:
		if p.fmt.sharpV {
			p.buf.writeString(f.Type().String())
		}
		p.buf.writeByte('{')
		for i := 0; i < f.NumField(); i++ {
			if i > 0 {
				if p.fmt.sharpV {
					p.buf.writeString(commaSpaceString)
				} else {
					p.buf.writeByte(' ')
				}
			}
			if p.fmt.plusV || p.fmt.sharpV {
				if name := f.Type().Field(i).Name; name != "" {
					p.buf.writeString(name)
					p.buf.writeByte(':')
				}
			}
			p.printValue(getField(f, i), verb, depth+1)
		}
		p.buf.writeByte('}')

이러한 코드 방식을 거쳐서 default 값을 만들어준다.

 

내가 원하는 방식으로 string을 출력하고 싶다면 String()이라는 메소드를 만들어주면 된다. override와 비슷한 방식이라고 생각한다. 아래와 같은 메소드를 정의준 뒤, 출력해주자.

// String 메소드 정의
func (a Account) String() string {
	return fmt.Sprintf("Owner: %s, Balance: %d", a.owner, a.balance)
}

출력

이전과는 다른 방식으로 출력이 되는 것을 볼 수 있다. 하지만, field가 추가되는 경우 다시 수정해줘야하는 단점이 존재할 것이다. 기본적으로 제공하는 것을 사용하는 방법이 좋아 보이긴 하다.

 

값을 수정할 수 있는 method를 만들어보려 한다. Go에서 method는 function과는 조금 다르다. 바로 receiver를 추가해줘야 한다. func과 함수명 사이에 receiver를 추가해주면, method가 된다. receiver에서 지켜야 할 사항이 있는데 receiver명은 struct의 첫글자를 따서 사용한다는 것이다. 편의를 위해서 정해놓은 암묵적인 rule 인것 같다. 물론, 지키지않아도 사용가능하다!

// make method
// func receiver name(prameter)
func (a *Account) Deposit(amount int) {
	a.balance += amount
}

위와 같은 method를 이용하여 struct의 값을 private로 설정해도 수정할 수 있다.

 

출력된 balance 값을 확인하기 위해 아래와 같은 method를 추가해주었다.

func (a Account) Balance() int {
	return a.balance
}

확인해본 결과 10이 추가된다.

tip)
(a *Account) 에서 *를 붙여준 이유는 *를 붙여줘야지만 내가 설정해놓은 struct를 receiver로 가져오기 때문이다. *를 제외하면 struct를 복사해서 가져오기때문에 수정한 값이 적용되지 않는다.

 

 

 

Error handling

Go에서는 exception, try-catch와 같은 에러 핸들링 도구가 존재하지 않는다. 그렇기때문에 직접 error를 직접 체크하고 return 해줘야 한다. 아래와 같은 기능을 하는 Withdraw 메소드를 만든다.

func(a *Account) Withdraw(amount int) {
	a.balance -= amount
}

이때, a.balance 값이 amount 값보다 작은 경우 작업을 하지않고 error를 return 해주려고 한다.

func(a *Account) Withdraw(amount int) error {
	if a.balance < amount {
		return errors.New("Can`t withdraw")
	}
	a.balance -= amount
	return nil
}

위와같이 작성해주면 된다. Go에서 error는 error와 nil이라는 값으로 나타낼 수 있다. 보통 nil은 에러가 없다는 값으로 사용한다. main에서 한 번 확인해보자.

func main() {
	account := accounts.NewAccount("qazyj")
	account.Deposit(10)
	fmt.Println(account.Balance())
	err := account.Withdraw(20)
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(account.Balance())
}

출력

balance값보다 큰 값으로 Withdraw를 했기때문에 nil이 아닌 error가 return이 되었고, if문을 통해 err가 nil이 아닌 경우 출력하여 제대로 error handling을 하는지 확인했다.

Go에서는 error가 발생했다고 시스템을 종료시키거나 하지 않는다. 순전히 개발자가 error를 스스로 return해주고, error에 관한 처리를 스스로 해주어야만 한다. 그렇기때문에 error를 return해도 아래 코드들이 정상적으로  실행된다.

 

에러를 로그로 남기며 시스템을 종료하고 싶다면, 아래와 같이 handling할 수 있다.

func main() {
	account := accounts.NewAccount("qazyj")
	account.Deposit(10)
	fmt.Println(account.Balance())
	err := account.Withdraw(20)
	if err != nil {
		log.Fatalln(err)
	}
	fmt.Println(account.Balance())
}

출력

error가 return되었고, error가 return되었을 때 log를 찍으며 치명적인 로그이기때문에 시스템을 종료하도록 하는 것이다. 시스템이 정상적으로 돌아갔다면 10이 한번 더 출력이 되어야 하지만, 바로 종료되었기 때문에 출력이되지 않는 모습이다. 

'Go' 카테고리의 다른 글

URL Checker & Go Routine  (0) 2024.01.14
method를 활용한 map 구현(Search, Add, Delete, Update)  (0) 2024.01.10
Struct  (0) 2024.01.04
Maps  (0) 2023.12.30
Arrays/Slices  (0) 2023.12.29

Struct

이전에 map에 대해배웠다. map의 경우 key와 value의 타입이 고정되어 있기 때문에, 아래와 같은 형태는 컴파일 에러가 발생한다.

user := map[string]string{"name":"qazyj", "age":1}

이러한 경우 Struct라는 구조체를 이용해서 관리하면 된다. map보다 동적이며, 원하는 type으로 생성할 수 있다. Java의 class와 같은 느낌이다.

 

테스트

type person struct {
	name string
	age int
	favFood []string
}

func main() {
	favFood := []string{"ramen", "kimchi"}
	user := person{"qazyj", 20, favFood}
	fmt.Println(user)
}

출력

func main() {
	favFood := []string{"ramen", "kimchi"}
	user := person{"qazyj", 20, favFood}
	fmt.Println(user.name)
}

출력

 

하지만, 위와같이 structure를 만드는 것은 코드상에서 보기 좋지 않아 보인다. (명확하게 보이지 않기 때문이다. 어떤 value가 어떤 value인지.. 값을 찾기 위해서는 정의되어있는 person을 한 번 더 봐야하기 때문이다.) 그렇기때문에 아래와 같은 방식이 처음 쓸 때는 불편하지만, 이후에 다시 코드를 볼 때 읽기 편리하다.

func main() {
	favFood := []string{"ramen", "kimchi"}
	user := person{
		name: "qazyj", 
		age: 20, 
		favFood: favFood}
	fmt.Println(user)
}

각각 어떤 value가 어떤 value인지 person이라는 struct를 보지않아도 한 눈에 확인할 수 있다. Java보다 가독성이 뛰어난 느낌이 든다.

 

Go는 java처럼 class가 없다. 또한, python이나 js처럼 object도 없다. Java에서 class를 생성할 때 필수로 있어야하는 것이 생성자이다. Go에서는 생성자가 없다.

 

 

 

 

 

 

 

'Go' 카테고리의 다른 글

method를 활용한 map 구현(Search, Add, Delete, Update)  (0) 2024.01.10
struct/public,private/String()/error handling  (0) 2024.01.07
Maps  (0) 2023.12.30
Arrays/Slices  (0) 2023.12.29
Pointer  (0) 2023.12.28

Map

Python이나 JavaScript의 Object와 약간 비슷한 형태이다.

테스트

func main() {
	// map[key type]value type{}
	name := map[string]string{"name":"qazyj", "address":"Incheon"}
	fmt.Println(name)
}

출력

아래와 같이 for문을 이용해서 출력할 수도 있다.

테스트

func main() {
	// map[key type]value type{}
	user := map[string]string{"name":"qazyj", "address":"Incheon"}
	for key, value := range user {
		fmt.Println(key, value)
	}
}

출력

 

 

 

 

 

'Go' 카테고리의 다른 글

struct/public,private/String()/error handling  (0) 2024.01.07
Struct  (0) 2024.01.04
Arrays/Slices  (0) 2023.12.29
Pointer  (0) 2023.12.28
If/switch  (0) 2023.12.28

+ Recent posts