20200913のGoに関する記事は8件です。

AtCoder Beginner Contest 178のメモ

前置き

Atcoderをやってみたので、自分用のメモです。
あとから加筆・修正する予定です。

問題

https://atcoder.jp/contests/abc176

A

Q_A.go
package main

import (
    "fmt"
)

func main() {
    var x int
    fmt.Scanf("%d", &x)

    var c int

    if x == 0{
        c = 1
    } else {
        c = 0
    }
    fmt.Printf("%d\n", c)
}

B

Q_B.go
package main

import (
    "fmt"
)

func main() {
    var a, b, c, d int
    fmt.Scanf("%d %d %d %d", &a, &b, &c, &d)

    var ac, ad, bc, bd int 

    ac = a * c
    ad = a * d
    bc = b * c
    bd = b * d

    var ans int 
    if ac < ad{
        ans = ad
    } else {
        ans = ac
    }

    if ans < bc{
        ans = bc
    } 

    if ans < bd{
        ans = bd
    }

    fmt.Printf("%d\n", ans)
}

C

Q_C.go
package main

import (
    "fmt"
)

func main() {
    var n int
    fmt.Scanf("%d", &n)

    var ans int64 = 0
    if n<2{
        ans = 0
    } else {
        var t int64 = 1
        var u int64 = 1
        var s int64 = 1
        for i:= 0; i < n; i++{
            s = (s * 9) % 1000000007
            t = (t * 8) % 1000000007
            u = (u * 10) % 1000000007
        }

        ans = u - s - s + t

    }

    ans = ans % 1000000007

    fmt.Printf("%d\n", ans)
}

D

覚えてたら後で書きます。

E

覚えてたら後で書きます。

F

覚えてたら後で書きます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

同じ色のPodが4個くっついたらdeleteされるcustom controller「くべくべ」を作った

Kubernetes使ってると、Nodeにえらい数のPodが溜まってくじゃないですか。消したくなりますよね。連鎖してほしいですよね。なりません?なので、4つ同じ色のPodが4個くっついたらdeleteされる、爽快感のあるカオスエンジニアリング用のcustom contollerを作りました。

deleteされるだけでは寂しいので、deleteされていく様子を見るためのkubectl pluginも作りました。合わせて使うとこんな感じになります。

kubectl-kbkb-rensa.gif

左側の●のひとつひとつがPodです。Nodeが列に対応してます。6Node構成です。各色8個ずつpodを立てていて、右側にreplicasetの増減を置いてみました。

レポジトリはこちらです。

https://github.com/omakeno/kubectl-kbkb
https://github.com/omakeno/kbkb-controller

使い方と実装を説明します。使用は自己責任でお願いします。

動作

横軸をNode、縦軸をPodとして二次元のフィールドにPodが配置されてると見立てて動作します。それぞれcreationTimestampでソートされています。

特定のAnnotationからPodの色を判定し、周囲との隣接数を数えて必要数を超えていたら、それらのPodを削除します。AnnotationがないPodは白と判定されます。白はくっついても消えないですし、周りの色Podに巻き込まれて消えることもありません。

Annotationはkbkb.k8s.omakenoyouna.net/colorです。Podごとに設定してください。red, green, yellow, blue, purpleが使えます。

metadata:
  annotations:
    kbkb.k8s.omakenoyouna.net/color: blue

AnnotationがPodに設定されていると、kubectl kbkbで色がついた状態で表示されます。bashでしか試していません。
等幅フォント前提です。-Lオプションで見やすく全角表示することができます。
image.png

-wオプションでwatchできます。Podが消えていく様を見ることができます。
kubectl-kbkb-watch.gif

使い方

kbkb-controller

kbkb-controllerと関連オブジェクトをdeployします。

kubectl apply -f https://raw.githubusercontent.com/omakeno/kbkb-controller/master/deploy/deploy.yaml

適用したいnamespace内にkbkbオブジェクトを作成します。
下記は「4個消し」ですが、「2個消し」「6個消し」などの設定も可能です。

apiVersion: k8s.omakenoyouna.net/v1beta1
kind: Kbkb
metadata:
  name: kbkb-four
spec:
  kokeshi: 4

これだけです。

kubectl kbkb

バイナリを落として、pathが通るようにしてください。
kubectlのpluginの機構で、kubectl-xxxxにpathが通っていると、kubectl xxxx のようにサブコマンドとして使えます。

wget https://github.com/omakeno/kubectl-kbkb/releases/download/v0.2.3/kubectl-kbkb
chmod +x kubectl-kbkb
sudo cp kubectl-kbkb <your-path>

あとは叩くだけです。

kubectl kbkb

--watch,-w, --namespace,-n, --large,-L, --kubeconfigのオプションがあります。

実装

ここでは詳細な解説はせず、紹介程度にします。コード量も少ないので、気になる方はリポジトリを見てみてください。
別途記録用に記事を書くかもです。

kbkb-controller

いわゆるカスタムコントローラーです。podをwatchして、4つ隣接した同色のPodを見つけてdeleteします。

リポジトリはここです。
https://github.com/omakeno/kbkb-controller

Operator SDKをgolangで利用しています。Tutorialに沿って進めばめっちゃ簡単です。
https://sdk.operatorframework.io/

初心者でもTutorialに沿って進めばほぼほぼ完成されたコードを吐き出してくれるので、Reconcileのfunctionだけ実装すれば動きます。この1個の関数だけに処理をゴリゴリ書いています。かんたん。
それ以外のコードはほとんど自動生成されたものをそのまま使っているだけです。

func (r *KbkbReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
    ctx := context.Background()
    reqLogger := r.Log.WithValues("pod", req.NamespacedName)

    reqLogger.Info("Reconciling")

    // reconcileされたオブジェクトのNamespace
    listOption := &client.ListOptions{
        Namespace: req.Namespace,
    }

    // 同一Namespace内のkbkbを取得
    kbkbList := &k8sv1beta1.KbkbList{}
    if err := r.Client.List(ctx, kbkbList, listOption); err != nil {
        reqLogger.Error(err, "failed to get kbkb")
        return ctrl.Result{}, err
    }
    if len(kbkbList.Items) == 0 {
        reqLogger.Info("kbkb not found. Ignore not found")
        return ctrl.Result{}, nil
    }
    kbkbObj := kbkbList.Items[0]
    kokeshi := *(kbkbObj.Spec.Kokeshi)

    // Pod一覧とNode一覧を取得
    podList := &corev1.PodList{}
    if err := r.Client.List(ctx, podList, listOption); err != nil {
        reqLogger.Error(err, "failed to get list of pods")
        return ctrl.Result{}, err
    }

    nodeList := &corev1.NodeList{}
    if err := r.Client.List(ctx, nodeList); err != nil {
        reqLogger.Error(err, "failed to get list of nodes")
        return ctrl.Result{}, err
    }

    // 隣接判定
    kf := kbkb.BuildKbkbFieldFromList(podList, nodeList)
    if !kf.IsStable() {
        reqLogger.Info("All containers are not Ready.")
        return ctrl.Result{}, nil
    }

    erasablePods := kf.ErasableKbkbPodList(kokeshi)

    //podの削除
    for _, kp := range erasablePods {
        pod := &corev1.Pod{
            ObjectMeta: metav1.ObjectMeta{
                Namespace: kp.ObjectMeta.Namespace,
                Name:      kp.ObjectMeta.Name,
            },
        }

        if err := r.Client.Delete(ctx, pod); err != nil {
            reqLogger.Error(err, "failed to delete pod", "deleteing pod", pod.ObjectMeta.Name)
        } else {
            reqLogger.Info("suceeded to delete pod", "deleted pod", pod.ObjectMeta.Name)
        }
    }

    return ctrl.Result{}, nil
}

隣接判定は別パッケージに切り出しました。以前みた実装を参考にしつつ、今回は深さ優先探索で判定してます。
https://github.com/omakeno/kbkb

func (kf *KbkbField) ErasableKbkbPodList(kokeshi int) []*KbkbPod {
    checkedPods := []*KbkbPod{}
    erasablePods := []*KbkbPod{}

    for x, col := range *kf {
        for y, _ := range col.kbkbs {
            var neighborPods []*KbkbPod
            neighborPods, checkedPods = kf.getNeighbors(x, y, checkedPods)
            if len(neighborPods) >= kokeshi {
                erasablePods = append(erasablePods, neighborPods...)
            }
        }
    }
    return erasablePods
}

// 再帰で深さ優先探索する関数
func (kf *KbkbField) getNeighbors(x int, y int, checkedPods []*KbkbPod) (neighborPods, checkedPodsAfter []*KbkbPod) {
    p := kf.GetKbkbPod(x, y)
    neighborPods = []*KbkbPod{p}
    if contains(checkedPods, p) {
        checkedPodsAfter = checkedPods
        return
    }
    checkedPodsAfter = append(checkedPods, p)

    if p.Color() == "white" {
        return
    }

    // 上下左右のpodを走査
    neighborPos := [][]int{
        {x + 1, y},
        {x - 1, y},
        {x, y + 1},
        {x, y - 1},
    }
    for _, pos := range neighborPos {
        if np := kf.GetKbkbPod(pos[0], pos[1]); np != nil && !contains(checkedPodsAfter, np) && np.Color() == p.Color() {
            var neighborPodsHere []*KbkbPod
            neighborPodsHere, checkedPodsAfter = kf.getNeighbors(pos[0], pos[1], checkedPodsAfter)
            neighborPods = append(neighborPods, neighborPodsHere...)
        }
    }
    return
}

というわけで作りは簡単、とはいえcontroller-runtimeなどのパッケージは使えないといけないですし、kubebuilderのマーカーについても抑えとかなきゃです。私は@go_vargoさんの書籍で一通り学習しました。やりたいことはこの中で全部書いてありました。
https://booth.pm/ja/items/1566979

書籍の中でoperator-sdkの解説もありますが、現在はバージョンが上がっていて、コマンド体系も変わっているので注意が必要です。

またgolang自体も初心者だったので、プログラミング言語Go完全入門にもお世話になりました。
https://drive.google.com/file/d/1fLlg3Xw7CV680GQ65WkjxU5qX-PsApJg/view

kubectl-kbkb

kubectlのプラグインです。単なるシングルバイナリのCLIツールです。
4個くっついたらdeleteされたくなるような見た目でpodを表示することができます。
Krewに入れてもらうのはさすがに無理かなあと思って諦めてます。Krewもカスタムのリポジトリが使えるようになったみたいなので、こういうネタツールでも入れてくれるリポジトリがあったらいいなあ。

リポジトリはここです。
https://github.com/omakeno/kubectl-kbkb

こちらはあまりまとまった情報がなかったのですが、下記をベースにいじって出来ました。
https://github.com/kubernetes/sample-cli-plugin

cobraをシンプルに使います。cli-runtimeを使うと良いらしいのですが、今回は使ってません。それでもo.Executeを実装すればcliツールが簡単に作れます。
あとは公式パッケージであるclient-goの使い方さえわかれば、controller同様に書けます。golangは入力補完でなんとかなりますね。その分だけドキュメントは弱めですが。

ちょっとだけ抜粋して載せます。

func CreateCmd() *cobra.Command {
    // コマンドを定義
    o := NewKbkbOptions()
    var rootCmd = &cobra.Command{
        Use:          "kbkb [flags]",
        Short:        "Show pods as kbkb format.",
        Example:      fmt.Sprintf(kbkbExample, "kubectl"),
        SilenceUsage: true,
        RunE: func(c *cobra.Command, args []string) error {
            if err := o.Execute(c, args); err != nil {
                return err
            }

            return nil
        },
    }

    // オプションをフラグとして設定
    rootCmd.PersistentFlags().StringVarP(&o.namespace, "namespace", "n", "default", "specify namespace to show as kbkb format.")
    rootCmd.PersistentFlags().BoolVarP(&o.watch, "watch", "w", false, "watch kbkb")
    rootCmd.PersistentFlags().StringVarP(&o.kubeconfig, "kubeconfig", "", filepath.Join(homeDir(), ".kube", "config"), "(optional) absolute path to the kubeconfig file")
    rootCmd.PersistentFlags().BoolVarP(&o.large, "large", "L", false, "view on large size")
    return rootCmd
}

func (o *KbkbOptions) Execute(cmd *cobra.Command, args []string) error {

    config, err := clientcmd.BuildConfigFromFlags("", o.kubeconfig)
    if err != nil {
        panic(err.Error())
    }

    clientset, err := kubernetes.NewForConfig(config)
    if err != nil {
        panic(err.Error())
    }

    if o.watch {
        o.Watch(clientset)
    } else {
        o.Get(clientset)
    }

    return nil
}
func (o *KbkbOptions) Get(clientset *kubernetes.Clientset) {
    // pod, nodeの一覧取得
    podList, err := clientset.CoreV1().Pods(o.namespace).List(context.TODO(), metav1.ListOptions{})
    if err != nil {
        panic(err.Error())
    }
    nodeList, err := clientset.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{})
    if err != nil {
        panic(err.Error())
    }

    // 描画処理(kbkbパッケージを使うだけ)
    kf := kbkb.BuildKbkbFieldFromList(podList, nodeList)
    writer := bashoverwriter.GetBashoverwriter()
    var kcs kbkb.KbkbCharSet
    if o.large {
        kcs = kbkb.GetKbkbCharSetWide()
    } else {
        kcs = kbkb.GetKbkbCharSet()
    }
    kcs.PrintKbkb(&writer, kf)
}

肝心の出力のフォーマットも、kbkbパッケージに寄せちゃってるので、こっちは至ってシンプルです。

まとめ

というわけで、削除処理と可視化はできました。
client-goやcontroller-runtimeやらのパッケージにkubebuilderやらoperator-sdkやら、すごく綺麗に整理されているので本当に簡単に実装が出来ます。ありがたい。

さて、まだやりたいことが残ってます。
せっかくCNDT2020で話を聞いたのでOperator Lifecycle Managerとかで使えるようにしてみるのもいいのですが、まずは機能。

  • 全てのPodがRunningになるとPodを2つ生成してくれるCustom Controller
  • createされるpodにランダムでAnnotationを付与するAdmission Controller
  • Queuingされるpodを2個ずつ操作して手動でnodeにschedulingするCustom Scheduler

2つずつ生成されるpodを上から落として積み上げて行きたくなりますよね。
めざせ19連鎖!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

kubernetesでもぷよぷよがしたいので同じ色のPodが4個くっついたらdeleteされるcustom controller「くべくべ」を作った

Kubernetes使ってると、Nodeにえらい数のPodが溜まってくじゃないですか。消したくなりますよね。連鎖してほしいですよね。なりません?なので、4つ同じ色のPodが4個くっついたらdeleteされる、爽快感のあるカオスエンジニアリング用のcustom contollerを作りました。

deleteされるだけでは寂しいので、deleteされていく様子を見るためのkubectl pluginも作りました。合わせて使うとこんな感じになります。

kubectl-kbkb-rensa.gif

左側の●のひとつひとつがPodです。Nodeが列に対応してます。6Node構成です。各色8個ずつpodを立てていて、右側にreplicasetの増減を置いてみました。

レポジトリはこちらです。

https://github.com/omakeno/kubectl-kbkb
https://github.com/omakeno/kbkb-controller

使い方と実装を説明します。使用は自己責任でお願いします。

動作

横軸をNode、縦軸をPodとして二次元のフィールドにPodが配置されてると見立てて動作します。それぞれcreationTimestampでソートされています。

特定のAnnotationからPodの色を判定し、周囲との隣接数を数えて必要数を超えていたら、それらのPodを削除します。AnnotationがないPodは白と判定されます。白はくっついても消えないですし、周りの色Podに巻き込まれて消えることもありません。

Annotationはkbkb.k8s.omakenoyouna.net/colorです。Podごとに設定してください。red, green, yellow, blue, purpleが使えます。

metadata:
  annotations:
    kbkb.k8s.omakenoyouna.net/color: blue

AnnotationがPodに設定されていると、kubectl kbkbで色がついた状態で表示されます。bashでしか試していません。
等幅フォント前提です。-Lオプションで見やすく全角表示することができます。
image.png

-wオプションでwatchできます。Podが消えていく様を見ることができます。
kubectl-kbkb-watch.gif

使い方

kbkb-controller

kbkb-controllerと関連オブジェクトをdeployします。

kubectl apply -f https://raw.githubusercontent.com/omakeno/kbkb-controller/master/deploy/deploy.yaml

適用したいnamespace内にkbkbオブジェクトを作成します。
下記は「4個消し」ですが、「2個消し」「6個消し」などの設定も可能です。

apiVersion: k8s.omakenoyouna.net/v1beta1
kind: Kbkb
metadata:
  name: kbkb-four
spec:
  kokeshi: 4

これだけです。

kubectl kbkb

バイナリを落として、pathが通るようにしてください。
kubectlのpluginの機構で、kubectl-xxxxにpathが通っていると、kubectl xxxx のようにサブコマンドとして使えます。

wget https://github.com/omakeno/kubectl-kbkb/releases/download/v0.2.3/kubectl-kbkb
chmod +x kubectl-kbkb
sudo cp kubectl-kbkb <your-path>

あとは叩くだけです。

kubectl kbkb

--watch,-w, --namespace,-n, --large,-L, --kubeconfigのオプションがあります。

実装

ここでは詳細な解説はせず、紹介程度にします。コード量も少ないので、気になる方はリポジトリを見てみてください。
別途記録用に記事を書くかもです。

kbkb-controller

いわゆるカスタムコントローラーです。podをwatchして、4つ隣接した同色のPodを見つけてdeleteします。

リポジトリはここです。
https://github.com/omakeno/kbkb-controller

Operator SDKをgolangで利用しています。Tutorialに沿って進めばめっちゃ簡単です。
https://sdk.operatorframework.io/

初心者でもTutorialに沿って進めばほぼほぼ完成されたコードを吐き出してくれるので、Reconcileのfunctionだけ実装すれば動きます。この1個の関数だけに処理をゴリゴリ書いています。かんたん。
それ以外のコードはほとんど自動生成されたものをそのまま使っているだけです。

func (r *KbkbReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
    ctx := context.Background()
    reqLogger := r.Log.WithValues("pod", req.NamespacedName)

    reqLogger.Info("Reconciling")

    // reconcileされたオブジェクトのNamespace
    listOption := &client.ListOptions{
        Namespace: req.Namespace,
    }

    // 同一Namespace内のkbkbを取得
    kbkbList := &k8sv1beta1.KbkbList{}
    if err := r.Client.List(ctx, kbkbList, listOption); err != nil {
        reqLogger.Error(err, "failed to get kbkb")
        return ctrl.Result{}, err
    }
    if len(kbkbList.Items) == 0 {
        reqLogger.Info("kbkb not found. Ignore not found")
        return ctrl.Result{}, nil
    }
    kbkbObj := kbkbList.Items[0]
    kokeshi := *(kbkbObj.Spec.Kokeshi)

    // Pod一覧とNode一覧を取得
    podList := &corev1.PodList{}
    if err := r.Client.List(ctx, podList, listOption); err != nil {
        reqLogger.Error(err, "failed to get list of pods")
        return ctrl.Result{}, err
    }

    nodeList := &corev1.NodeList{}
    if err := r.Client.List(ctx, nodeList); err != nil {
        reqLogger.Error(err, "failed to get list of nodes")
        return ctrl.Result{}, err
    }

    // 隣接判定
    kf := kbkb.BuildKbkbFieldFromList(podList, nodeList)
    if !kf.IsStable() {
        reqLogger.Info("All containers are not Ready.")
        return ctrl.Result{}, nil
    }

    erasablePods := kf.ErasableKbkbPodList(kokeshi)

    //podの削除
    for _, kp := range erasablePods {
        pod := &corev1.Pod{
            ObjectMeta: metav1.ObjectMeta{
                Namespace: kp.ObjectMeta.Namespace,
                Name:      kp.ObjectMeta.Name,
            },
        }

        if err := r.Client.Delete(ctx, pod); err != nil {
            reqLogger.Error(err, "failed to delete pod", "deleteing pod", pod.ObjectMeta.Name)
        } else {
            reqLogger.Info("suceeded to delete pod", "deleted pod", pod.ObjectMeta.Name)
        }
    }

    return ctrl.Result{}, nil
}

隣接判定は別パッケージに切り出しました。以前みた実装を参考にしつつ、今回は深さ優先探索で判定してます。
https://github.com/omakeno/kbkb

func (kf *KbkbField) ErasableKbkbPodList(kokeshi int) []*KbkbPod {
    checkedPods := []*KbkbPod{}
    erasablePods := []*KbkbPod{}

    for x, col := range *kf {
        for y, _ := range col.kbkbs {
            var neighborPods []*KbkbPod
            neighborPods, checkedPods = kf.getNeighbors(x, y, checkedPods)
            if len(neighborPods) >= kokeshi {
                erasablePods = append(erasablePods, neighborPods...)
            }
        }
    }
    return erasablePods
}

// 再帰で深さ優先探索する関数
func (kf *KbkbField) getNeighbors(x int, y int, checkedPods []*KbkbPod) (neighborPods, checkedPodsAfter []*KbkbPod) {
    p := kf.GetKbkbPod(x, y)
    neighborPods = []*KbkbPod{p}
    if contains(checkedPods, p) {
        checkedPodsAfter = checkedPods
        return
    }
    checkedPodsAfter = append(checkedPods, p)

    if p.Color() == "white" {
        return
    }

    // 上下左右のpodを走査
    neighborPos := [][]int{
        {x + 1, y},
        {x - 1, y},
        {x, y + 1},
        {x, y - 1},
    }
    for _, pos := range neighborPos {
        if np := kf.GetKbkbPod(pos[0], pos[1]); np != nil && !contains(checkedPodsAfter, np) && np.Color() == p.Color() {
            var neighborPodsHere []*KbkbPod
            neighborPodsHere, checkedPodsAfter = kf.getNeighbors(pos[0], pos[1], checkedPodsAfter)
            neighborPods = append(neighborPods, neighborPodsHere...)
        }
    }
    return
}

というわけで作りは簡単、とはいえcontroller-runtimeなどのパッケージは使えないといけないですし、kubebuilderのマーカーについても抑えとかなきゃです。私は@go_vargoさんの書籍で一通り学習しました。やりたいことはこの中で全部書いてありました。
https://booth.pm/ja/items/1566979

書籍の中でoperator-sdkの解説もありますが、現在はバージョンが上がっていて、コマンド体系も変わっているので注意が必要です。

またgolang自体も初心者だったので、プログラミング言語Go完全入門にもお世話になりました。
https://drive.google.com/file/d/1fLlg3Xw7CV680GQ65WkjxU5qX-PsApJg/view

kubectl-kbkb

kubectlのプラグインです。単なるシングルバイナリのCLIツールです。
4個くっついたらdeleteされたくなるような見た目でpodを表示することができます。
Krewに入れてもらうのはさすがに無理かなあと思って諦めてます。Krewもカスタムのリポジトリが使えるようになったみたいなので、こういうネタツールでも入れてくれるリポジトリがあったらいいなあ。

リポジトリはここです。
https://github.com/omakeno/kubectl-kbkb

こちらはあまりまとまった情報がなかったのですが、下記をベースにいじって出来ました。
https://github.com/kubernetes/sample-cli-plugin

cobraをシンプルに使います。cli-runtimeを使うと良いらしいのですが、今回は使ってません。それでもo.Executeを実装すればcliツールが簡単に作れます。
あとは公式パッケージであるclient-goの使い方さえわかれば、controller同様に書けます。golangは入力補完でなんとかなりますね。その分だけドキュメントは弱めですが。

ちょっとだけ抜粋して載せます。

func CreateCmd() *cobra.Command {
    // コマンドを定義
    o := NewKbkbOptions()
    var rootCmd = &cobra.Command{
        Use:          "kbkb [flags]",
        Short:        "Show pods as kbkb format.",
        Example:      fmt.Sprintf(kbkbExample, "kubectl"),
        SilenceUsage: true,
        RunE: func(c *cobra.Command, args []string) error {
            if err := o.Execute(c, args); err != nil {
                return err
            }

            return nil
        },
    }

    // オプションをフラグとして設定
    rootCmd.PersistentFlags().StringVarP(&o.namespace, "namespace", "n", "default", "specify namespace to show as kbkb format.")
    rootCmd.PersistentFlags().BoolVarP(&o.watch, "watch", "w", false, "watch kbkb")
    rootCmd.PersistentFlags().StringVarP(&o.kubeconfig, "kubeconfig", "", filepath.Join(homeDir(), ".kube", "config"), "(optional) absolute path to the kubeconfig file")
    rootCmd.PersistentFlags().BoolVarP(&o.large, "large", "L", false, "view on large size")
    return rootCmd
}

func (o *KbkbOptions) Execute(cmd *cobra.Command, args []string) error {

    config, err := clientcmd.BuildConfigFromFlags("", o.kubeconfig)
    if err != nil {
        panic(err.Error())
    }

    clientset, err := kubernetes.NewForConfig(config)
    if err != nil {
        panic(err.Error())
    }

    if o.watch {
        o.Watch(clientset)
    } else {
        o.Get(clientset)
    }

    return nil
}
func (o *KbkbOptions) Get(clientset *kubernetes.Clientset) {
    // pod, nodeの一覧取得
    podList, err := clientset.CoreV1().Pods(o.namespace).List(context.TODO(), metav1.ListOptions{})
    if err != nil {
        panic(err.Error())
    }
    nodeList, err := clientset.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{})
    if err != nil {
        panic(err.Error())
    }

    // 描画処理(kbkbパッケージを使うだけ)
    kf := kbkb.BuildKbkbFieldFromList(podList, nodeList)
    writer := bashoverwriter.GetBashoverwriter()
    var kcs kbkb.KbkbCharSet
    if o.large {
        kcs = kbkb.GetKbkbCharSetWide()
    } else {
        kcs = kbkb.GetKbkbCharSet()
    }
    kcs.PrintKbkb(&writer, kf)
}

肝心の出力のフォーマットも、kbkbパッケージに寄せちゃってるので、こっちは至ってシンプルです。

まとめ

というわけで、削除処理と可視化はできました。
client-goやcontroller-runtimeやらのパッケージにkubebuilderやらoperator-sdkやら、すごく綺麗に整理されているので本当に簡単に実装が出来ます。ありがたい。

さて、まだやりたいことが残ってます。
せっかくCNDT2020で話を聞いたのでOperator Lifecycle Managerとかで使えるようにしてみるのもいいのですが、まずは機能。

  • 全てのPodがRunningになるとPodを2つ生成してくれるCustom Controller
  • createされるpodにランダムでAnnotationを付与するAdmission Controller
  • Queuingされるpodを2個ずつ操作して手動でnodeにschedulingするCustom Scheduler

2つずつ生成されるpodを上から落として積み上げて行きたくなりますよね。
めざせ19連鎖!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

A Tour of Go メモ 【3】1日目

 ポインタ、 構造体、 配列、 スライス

A Tour of Go

ポインタ

Golangのポインタ渡し初心者を卒業した

func main() {
    i, j ;= 10, 200

    # pはiの値を参照する 
    p := &i 

    # iとpのアドレスを取得する
    fmt.Println(&i)
    fmt.Println(&p)

   # pが参照している変数、つまり、iを表示する
    fmt.Println(*p)

    # pが参照する変数に21を代入する。つまり、i に 21 を代入する
    *p = 21
    fmt.Println(i)

    # pはjの値を参照する
    p = &j

    # pが参照する値に”pが参照する値を100で割った値”を代入する。
    # つまり、jに"jを100で割った値"を代入する
    *p = *p / 100
    fmt.Println(j)
   
}
// iを参照したpの値
>10 
// *pに21を代入した後のi
>21
// iのアドレス
>0xc000100010
// pのアドレス iを参照して値は同じだが、アドレスは違う
>0xc000102018 
>2

構造体

struct(構造体)はフィールド(field) の集まり

type Vertex struct {
  X int
  Y int
}

func main() {
   fmt.Println(Vertex{1, 2})
   v := Vertex{1, 2}
   fmt.Println(v.X)
   v.X = 4
   fmt.Println(v.X)

}

> {1, 2}
> 1
> 4

構造体とポインタ

type Vertex struct {
    X int
    Y int
}

func main() {
    v := Vertex{1, 2}
      fmt.Println(v)
        # pはv(Vertex)を参照する
    p := &v

        # pが参照しているVertexのXに値を代入する
    p.X = 1e9
    fmt.Println(v)
}
> {1 2}
// ポインタを通して、Xの値が再代入されている
> {1000000000 2}

Struct リテラル

type Vertex struct {
    X, Y int
}

var (
        v1 = Vertex{1, 2} 

        # Xには1を、Yは0(何も代入されていないので、初期値の0になる)
        v2 = Vertex{X: 1} 

        # X にも Y にも何も代入されていないので、初期値の0になる
        v3 = Vertex{}

        # ちょっとよくわからない・・・
        p  = &Vertex{1, 2}

)

func main() {
    fmt.Println(v1, p, v2, v3)
}

> {1 2} &{11 2} {1 0} {0 0}

配列

拡張不可なので、append関数で要素を追加できない

func main() {
       var a [2]string
       a[0] = "Hello"
       a[1] = "World"
       fmt.Println(a[0], a[1])
       fmt.Println(a)

       primes := [6]int{1,2,3,4,5,6}
       fmt.Println(primes)
}

> Hello World
> [Hello World]
> [2 3 5 7 11 13]

スライス

append関数で要素の追加が可能、つまり、拡張可能
```
func main() {
primes := [6]int{2, 3, 5, 7, 11, 13}

    var s []int = primes[1:4]
    fmt.Println(s)
    fmt.Println(primes[:1])
    fmt.Println(primes[2:])

}

[3 5 7]
[2 3 ]
[5 7 11 13]

  • スライスした後の配列に値を代入すると元の配列が変わる

func main() {
names := [4]string{
"John",
"Paul",
"George",
"Ringo",
}
fmt.Println(names)

a := names[0:2]
b := names[1:3]
fmt.Println(a, b)

b[0] = "XXX"
fmt.Println(a, b)
fmt.Println(names)
fmt.Println(names)

}

[John Paul George Ringo]
[John Paul] [Paul George]
[John XXX] [XXX George]
[John XXX George Ringo]
//元の配列が変わっている
[John XXX George Ringo]
```

スライス リテラル

func main() {
    q := []int{2, 3, 4, 5, 7, 11, 13}
    fmt.Println(q)

    r := []bool{true, false, true, true, false, true}
    fmt.Println(r)

    s := []struct{
        i int
        b bool
    }{
       {2, true}
       {3, false},
       {5, true},
       {7, true},
       {11, false},
       {13, true}
    }
    fmt.Println(s)
}
> [2 3 5 7 11 13]
> [true false true true false true]
> [{2 true} {3 false} {5 true} {7 true} {11 false} {13 true}]

 スライスのlengthとcapacity

length ・・・ 実際に配列に入っている要素の数
capacity ・・・ 確保されているメモリの領域
*配列とスライスは全くの別物
Go言語のスライスで勘違いしやすいこところ

func main() {
    s := []int{2, 3, 5, 7, 11, 13}
    printSlice(s)

    // Slice the slice to give it zero length.
    s = s[:5]
    printSlice(s)

    // Extend its length.
    s = s[:6]
    printSlice(s)

    // Drop its first two values.
    s = s[2:]
    printSlice(s)

    s = s[:5]
    printSlice(s)

    s = append(s, 4)
    printSlice(s)
}

func printSlice(s []int) {
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}

> len=6 cap=6 [2 3 5 7 11 13]

//要素を一つ削る、しかし、capacityは減っていない
> len=5 cap=6 [2 3 5 7 11]

//長さを戻すと削ったはずの要素が復活する
> len=6 cap=6 [2 3 5 7 11 13]

// ドロップするとlengthもcapacityも減っている
> len=4 cap=4 [5 7 11 13]

// capacity以上にlengthを伸ばそうとするとエラーになる
> panic: runtime error: slice bounds out of range [:5] with capacity 4

// capacityを超えて要素を追加しようとすると、capacityが以前の2倍になる
> len=5 cap=8 [5 7 11 13 4]


#sliceの初期値
func main() {
    var s []int
    fmt.Println(s, len(s), cap(s))
    if s == nil {
        fmt.Println("nil!")
    }
}

> [] 0 0
> nil!

スライスとmake

makeを使って、lengthとcapacityを指定して、スライスを生成できる
要素の初期値は0

func main() {
    a := make([]int, 5)
    printSlice("a", a)

    b := make([]int, 0, 5)
    printSlice("b", b)

    c := b[:2]
    printSlice("c", c)

    d := c[2:5]
    printSlice("d", d)
}

func printSlice(s string, x []int) {
    fmt.Printf("%s len=%d cap=%d %v\n",
        s, len(x), cap(x), x)
}

 スライスの中にスライス

import (
    "fmt"
       // 文字列の操作
    "strings"
)

func main() {
    // Create a tic-tac-toe board.
    board := [][]string{
        []string{"_", "_", "_"},
        []string{"_", "_", "_"},
        []string{"_", "_", "_"},
    }
    fmt.Println(board)

    board[0][0] = "X"
    board[2][2] = "O"
    board[1][2] = "X"
    board[1][0] = "O"
    board[0][2] = "X"

    for i := 0; i < len(board); i++ {

        // スライス内の要素を" "でつないで出力
        fmt.Printf("%s\n", strings.Join(board[i], " "))
    }

}

> [[_ _ _] [_ _ _] [_ _ _]]
> X _ X
> O _ X
> _ _ O
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go言語: 雑多な Tips、文法など

この記事について

忘備録として Go言語の雑多なTIPS、文法などを脈絡なくまとめていきます。

TIPS

ランダムな文字列の生成

いろんな方法があるが、長さや文字種の指定がなければこんな簡易的な方法もある(Unix時間を36進数表記の文字列に変換してる)。

s := strconv.FormatInt(time.Now().UnixNano(), 36)
// 実行結果は、例えば "c3av0t23ntsk"

ただ、時刻を用いてるだけなので複数の Goroutine から同時実行すると同じ文字列が返る可能があることは注意。

ある文字列が数字だけ含むかチェック

"20200601" のように、文字列が数字だけ含むかどうかチェックしたいときはstrings.Trim(<文字列>, "0123456789") == "" が使える。

サンプルコード:

func main() {
    params := []string {"1", "dog", "20200601", "5c6"}

    for _, p := range params {
        if strings.Trim(p, "0123456789") == "" {
            fmt.Println(p)
        }
    }
}

// 実行結果
// 1
// 20200601

if の代わりの switch

switch の後ろに変数を指定しないで、単に if 文の代わりにのように使うことが出来る。

type user struct {
    name    string
    age     int
    healthy bool
    hobbies []string
}

func main() {
    u := user{
        name:    "Andy",
        age:     110,
        healthy: true,
        hobbies: []string{"Game", "Music"},
    }

    // if, else の代わり
    switch {
    case u.name == "":
        fmt.Println("Unknown!")
    case u.age > 100 && u.healthy:
        fmt.Println("Fantastic!")
    case len(u.hobbies) > 0:
        fmt.Println("Have fun!")
    }
}

// 実行結果
// Fantastic!

main 関数を抜けたくない時

goroutineの開始後に main関数から抜けたくない時、select を使うことができる。

func main() {
    // goroutineを開始...

    // goroutineがずっと動いてるので、main から抜けたくない
    select {}
}

適当なサンプルだがこんな感じの使い方。goroutineが終了するケースでは all goroutines are asleep - deadlock! って怒られるので要注意。

package main

import (
    "fmt"
    "net/http"
)

func main() {
    // 複数の HTTP サーバーを起動
    addrs := []string{":8080", ":8081", ":8082"}
    for _, addr := range addrs {
        s := &http.Server{
            Addr: addr,
            Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                fmt.Fprintf(w, "Hello from %s", r.Host)
            }),
        }
        go s.ListenAndServe()
    }

    select {}
}

main 関数を抜けたくない時 (ユーザーの入力待ち状態にする)

似た話だが、mainを抜けたくないため入力待ち状態にする方法。ENTER キーを打つと main を抜ける。

import (
    "bufio"
    "os"
)

func main() {
    // goroutineを開始...

    // 入力待ち状態にする
    bufio.NewScanner(os.Stdin).Scan()
}

スライスの要素を削除

よくあるやつだが、スライスの途中の要素を削除する方法。

func main() {
    s := []string{"dog", "cat", "bird", "desk", "pig", "rabbit"}

    n := 3 // "desk" を削除したい
    s = append(s[:n], s[n+1:]...)
    fmt.Println(s)

    // append を使わずにこうやってもよい。
    //copy(s[n:], s[n+1:])
    //s = s[:len(s)-1]
}

// 実行結果
// [dog cat bird pig rabbit]

ある変数がインターフェースを実装しているかチェック

reflect パッケージの Implements を使う。

package main

import (
    "fmt"
    "reflect"
)

type Sender interface {
    Send()
}

type Receiver interface {
    Recv()
}

// App は Sender インターフェースだけを実装
type App struct{}

func (c *App) Send() {
    fmt.Println("Send something")
}

func main() {
    a := &App{}

    at := reflect.TypeOf(a)
    st := reflect.TypeOf((*Sender)(nil)).Elem()
    rt := reflect.TypeOf((*Receiver)(nil)).Elem()

    fmt.Printf("Client implements Sender   : %v\n", at.Implements(st))
    fmt.Printf("Client implements Receiver : %v\n", at.Implements(rt))
}

// 実行結果:
// Client implements Sender   : true
// Client implements Receiver : false

文法

make してない map へのアクセス

make してない map(つまりnil) のキーにアクセスすることが出来て、値として定義した型の初期値が返ってくる。

type user struct {
    name string
    age  int
}

func main() {
    // map を宣言
    var m map[string]user

    // m は nil のまま
    if m == nil {
        fmt.Println("m is nil")
    }

    // キーにアクセスすると、値として定義した型(user)の初期値が返る
    u := m["AAA"]
    fmt.Printf("%#v\n", u)

    // ただし、Key に Value をセットしようとすると panic する
    // -> panic: assignment to entry in nil map
    //m["BBB"] = user{name: "Andy", age: 20}
}

// 実行結果
// m is nil
// main.user{name:"", age:0}

型エイリアス

type構文と見た目が似てるが、型のエイリアスを定義する alias 構文というものがある。違いは以下。

// type構文 : NewString と string は異なる型として扱われる
type NewString string

// alias構文 : AliasString と string 同じ型として扱われる
type AliasString = string

サンプルコード:

type NewString string
type AliasString = string

func main() {
    s := "Hello"

    // 型が違うのでコンパイルエラーになる
    // → cannot use s (type string) as type NewString in assignment
    var ns NewString = s

    // こっちは OK
    var as AliasString = s
    fmt.Printf("%v\n", as)
}

ループ変数

Go のハマりどころのド定番。
Go ではループ変数(下記コードの n)はループが回っている間ずっと単一の変数、つまり、同じメモリ領域を使っている。ループが回るごとに変数の値が変わるだけで、変数(メモリ領域)そのものは同じ。

なので、以下のようにループ変数のアドレスを取得すると意図しない動作になる。

func main() {
    iNums := []int {1, 2, 3}
    var oNums []*int

    // ループ変数 n のアドレスを oNums に追加していく
    for _, n :=  range iNums {
        oNums = append(oNums, &n)
    }

    // oNums に追加された値とアドレスを確認
    for i, pn := range oNums {
        fmt.Printf("Index [%d], Value [%d], Address [%p]\n", i, *pn, pn)
    }
}

// 実行結果: Value は 1, 2, 3 にはならない。
// Index [0], Value [3], Address [0xc00002c008]
// Index [1], Value [3], Address [0xc00002c008]
// Index [2], Value [3], Address [0xc00002c008]

この場合、こんな風に対処できる(一部のみ掲載)。

    for _, n :=  range iNums {
        N := n
        oNums = append(oNums, &N)
    }

// 実行結果:
Index [0], Value [1], Address [0xc00002c008]
Index [1], Value [2], Address [0xc00002c040]
Index [2], Value [3], Address [0xc00002c048]

ループ変数と goroutine

Go のハマりどころのド定番 その2。
以下のコードは単一のループ変数 n を複数の goroutine が参照していること、かつ、n は goroutine 実行時の値として評価されるため意図した動作にならない。goroutine の実行タイミングによるが、ループが回り終わった後に実行されるとしたら全て最終要素の 3 になる。

iNums := []int{1, 2, 3}
for _, n := range iNums {
    go func() {
        fmt.Println(n)
    }()
}

// 実行結果: 1, 2, 3 にはならない
// 3
// 3
// 3

対処方法は、goroutine 定義時の値を引数として渡してあげること。

for _, n := range iNums {
    go func(N int) {
        fmt.Println(N)
    }(n)
}

これでもよい。

for _, n := range iNums {
    N := n
    go func() {
        fmt.Println(N)
    }()
}

クロージャー

クロージャーとは通常の関数とは少し異なり、関数の定義だけでなく、それが定義された際の環境(自身の外で宣言された変数)をセットしたもの。

以下、簡単な例。

// getFuncはクロージャーを生成する関数
func getFunc() func() {
    i := 0
    // 以下の func() がクロージャー
    return func() {
        // 自身の外で宣言された i にアクセスする
        i++
        fmt.Println(i)
    }
}

func main() {
    // クロージャーの生成と呼び出し(1つ目) : 1 → 2 → 3 と増える
    fn1 := getFunc()
    fn1()
    fn1()
    fn1()

    // クロージャーの生成と呼び出し(2つ目): また 1 から始まる
    fn2 := getFunc()
    fn2()
}

// 実行結果
// 1
// 2
// 3
// 1

ここで1つ前の「ループ変数と goroutine」の例を見てみる。

goroutine として実行される関数は、自身の外で使われてるループ変数 n を参照するクロージャー。ループ変数(この例のn)は全て同じメモリ領域を指すので、この例の3つの goroutine は全て同じ変数 n を参照する。各 goroutine の実行タイミングによるが、すべての出力が 3 になったりする。解決方法は前述のとおり goroutine の引数として n を渡すなど。

iNums := []int{1, 2, 3}
for _, n := range iNums {
    go func() {
        fmt.Println(n)
    }()
}

// 実行結果(タイミングによる)
// 3
// 3
// 3

(参考)
- https://golang.org/doc/faq#closures_and_goroutines
- https://golang.org/doc/effective_go.html#goroutines
- https://gobyexample.com/closures

コンパイラ

Heap と Stack

Go の変数はメモリ上の Heap と Stack のどちらに保存されるか?

  • コンパイラが自動で判断して、関数内からのみ参照される変数ならスタック、関数外から参照される可能性がある変数はヒープに割り当てる
  • ビルド時に go build -gcflags -m main.go というように -gcflags オプションを付けると、どちらが選択されたか見れる。

(引用) https://golang.org/doc/faq

From a correctness standpoint, you don't need to know. Each variable in Go exists as long as there are references to it. The storage location chosen by the implementation is irrelevant to the semantics of the language

The storage location does have an effect on writing efficient programs. When possible, the Go compilers will allocate variables that are local to a function in that function's stack frame. However, if the compiler cannot prove that the variable is not referenced after the function returns, then the compiler must allocate the variable on the garbage-collected heap to avoid dangling pointer errors. Also, if a local variable is very large, it might make more sense to store it on the heap rather than the stack.

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

deferの実行タイミング

タイミング

  • return
  • panic

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Go】http.HandleFunc()とhttp.Handle()の使い分けは構造体のフィールドを使うかどうか

はじめに

http.HandleFunc()とhttp.Handle()でServeHTTPを呼んでいるのに
なにが違っているのか分からなかったので調べてみました。

結論

構造体のフィールドを使いたいかどうか

http.HandleFunc()とhttp.Handle()

GoのドキュメントのPackage httpのよると

ListenAndServe starts an HTTP server with a given address and handler. The handler is usually nil, which means to use DefaultServeMux. Handle and HandleFunc add handlers to DefaultServeMux:

とどちらもDefaultServeMuxのパターンとしてハンドラを登録するもの。

DefaultServeMuxについてやハンドラとの関係性については以下の記事がわかりやすいです。
【Go】net/httpパッケージを読んでhttp.HandleFuncが実行される仕組み

http.HandleFunc()

ドキュメントの例では

package main

import (
    "io"
    "log"
    "net/http"
)

func main() {
    h1 := func(w http.ResponseWriter, _ *http.Request) {
        io.WriteString(w, "Hello from a HandleFunc #1!\n")
    }
    h2 := func(w http.ResponseWriter, _ *http.Request) {
        io.WriteString(w, "Hello from a HandleFunc #2!\n")
    }

    http.HandleFunc("/", h1)
    http.HandleFunc("/endpoint", h2)

    log.Fatal(http.ListenAndServe(":8080", nil))
}

という形で
/のパターンであればHello from a HandleFunc #1!を出力し
/endpointのパターンであればHello from a HandleFunc #2!を出力する

http.Handle()

ドキュメントの例では

package main

import (
    "fmt"
    "log"
    "net/http"
    "sync"
)

type countHandler struct {
    mu sync.Mutex // guards n
    n  int
}

func (h *countHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    h.mu.Lock()
    defer h.mu.Unlock()
    h.n++
    fmt.Fprintf(w, "count is %d\n", h.n)
}

func main() {
    http.Handle("/count", new(countHandler))
    log.Fatal(http.ListenAndServe(":8080", nil))
}

という形で
/countのパターンであればカウントの回数を出力する

この時にstructのフィールドn intに呼ばれた回数ごとに+1しているので
呼ばれるたびに回数を増加することができる。

まとめ

ServeHTTPを実装しhttp.Handle()を使うことでstruct内のフィールドを利用したハンドラ作成が可能となる。

参考文献

ありがとうございました。
GoのドキュメントのPackage http
【Go】net/httpパッケージを読んでhttp.HandleFuncが実行される仕組み
Go 言語の http パッケージにある Handle とか Handler とか HandleFunc とか HandlerFunc とかよくわからないままとりあえずイディオムとして使ってたのでちゃんと理解したメモ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GoでExcelを操作する

Goでエクセルファイルを操作する方法のメモです。
tealeg/xlsxライブラリを使うと簡単でした。

Gitのインポート

github上のライブラリを使用するため、Gitのダウンロードが必要でした。
※設定はデフォルトのままで行いました。

ライブラリのインポート

コマンドプロンプトで以下のコマンドを実行すると、エクセルのライブラリが使えるようになります。go get の後のURIはGoDocで検索して取得しました。

go get github.com/tealeg/xlsx

コマンドで実行するだけなので楽でした。

プログラム作成

テスト用として、エクセルにシートを追加するプログラムを作成しました。

<手順>
1. goファイルと同階層に新規ワークシートを作成
2. ファイル名を「テスト」とリネームして保存
3. 以下プログラムを実行

test.go
package main

import (
    "fmt"
    "time"

    "github.com/tealeg/xlsx"
)

func main() {
    dt1 := time.Now()

    excel, _ := xlsx.OpenFile("テスト.xlsx")

    excel.AddSheet("GOで作成")

    excel.Save("./new.xlsx")

    dt2 := time.Now()

    fmt.Println(dt2.Sub(dt1)) // 実行時間
}

実行すると、「テスト.xlsx」のコピーにシートが追加された状態で新しいファイル「new.xlsx」が同階層に保存されます。
※はじめは上書きをするプログラムを作成したのですが、PC内のセキュリティソフトに「疑わしい処理」としてブロックされてしまいました。上書きするには、エクセルシートのセキュリティの設定を下げる必要がありそうです。

プログラムの実行時間は12ミリ秒でした。
エクセルファイルを開いて、自身でシートを追加するより圧倒的に速いです。

使用関数

tealeg/xlsxライブラリ内から以下の関数を使用しました。

  • OpenFile

    func OpenFile(fileName string, options ...FileOption) (file *File, err error)

    OpenFile will take the name of an XLSX file and returns a populated xlsx.
    File struct for it. You may pass it zero, one or many FileOption functions that affect the behaviour of the file.

  • Addsheet

    func (f *File) AddSheet(sheetName string) (*Sheet, error)

    AddSheet Add a new Sheet, with the provided name, to a File.
    The minimum sheet name length is 1 character.
    If the sheet name length is less an error is thrown.
    The maximum sheet name length is 31 characters.
    If the sheet name length is exceeded an error is thrown.
    These special characters are also not allowed: : \ / ? * [ ]

  • Save

    func (f *File) Save(path string) (err error)

    Save the File to an xlsx file at the provided path.

今回はファイルやシートを扱う関数を使用しましたが、セルや列を扱う関数もあるようなので、
もう少し便利なプログラムを作れそうです。


▼参考にしたサイト
Go言語でExcelファイルを処理するのが超簡単だった

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む