3行
- 簡易アプリを作りながらGoの勉強をした
- gotoをみたぼく「おっほ♡」
- goroutineおもろい
Goってなんかよさげっぽくない?
おはようございます。デブです。
最近応用過去問を解こうとして毎回計算問題の暗算が出来ない面倒なのでとばしながらやっているデブです。
本番って電卓使えましたっけ……?使えたらあんな程度の問題では済まないと思うので使えないとは思いますが。
というわけで応用の勉強をしたくないのでの合間に触ったことのない言語を頭に入れておこう、というのが発端です。
対象はGo。
最近Rustに並んで「プログラマが勉強したい言語」みたいな趣旨でよく見る言語ですね。
なお以下の内容は扱いません。
- 環境構築
- メリデメ、向き不向き、使い所
- 言語仕様
成果物
内容は「4桁の数字を突っ込んだら10になる式を返してくれる謎API」です。
別に何でも良かったのですが、ボリューム軽めで並列処理を自然に扱うならこれかなと。
実際はどう考えてもスレッド化のオーバーヘッドの方が圧倒的にデカいのですが、まあ勉強なので。
ソース
package main
import (
"fmt"
"math"
"net/http"
"strings"
"sync"
"github.com/gin-gonic/gin"
)
const TargetAnswer = 10
const NumbersLength = 4
type Numbers [NumbersLength]float64
type Operator int
const (
Add Operator = iota
Subtract
Multiply
Divide
)
const OperatorPairLength = NumbersLength - 1
type OperatorPair [OperatorPairLength]Operator
func (o Operator) String() string {
//fmt対応
//toString相当
switch o {
case Add:
return "+"
case Subtract:
return "-"
case Multiply:
return "*"
case Divide:
return "/"
}
return ""
}
// 配列は定数にできないノーネ
var operators = []Operator{Add, Subtract, Multiply, Divide}
type Formula struct {
numbers *Numbers
operatorPair OperatorPair
value float64
}
func newFormula(numbers *Numbers, op OperatorPair) *Formula {
f := Formula{
numbers: numbers,
operatorPair: op,
value: math.NaN(),
}
f.calc()
return &f
}
func (f *Formula) calc() float64 {
stack := Numbers{0}
stack[0] = f.numbers[0]
index := 0
for i, o := range f.operatorPair {
n := f.numbers[i+1]
switch o {
case Add:
index++
stack[index] = n
case Subtract:
index++
stack[index] = -n
case Multiply:
stack[index] = stack[index] * n
case Divide:
if n == 0 {
stack[index] = math.NaN()
break
}
stack[index] = stack[index] / n
}
if math.IsNaN(stack[index]) {
return math.NaN()
}
}
value := 0.0
for _, v := range stack {
value += v
}
f.value = value
return value
}
func (f *Formula) String() string {
var sb strings.Builder
//%vで型ごとにデフォルト表記
fmt.Fprintf(&sb, "%v ", f.numbers[0])
for i, o := range f.operatorPair {
fmt.Fprintf(&sb, "%v %v ", o, f.numbers[i+1])
}
fmt.Fprintf(&sb, " = %v", f.value)
return sb.String()
}
func powInt(x int, y int) int {
//powがfloat64固定だったので自前で実装
n := 1
for i := 0; i < y; i++ {
n *= x
}
return n
}
func str2Numbers(str string) (isValid bool, numbers Numbers) {
isValid = false
if len([]rune(str)) != NumbersLength {
return
}
for i, c := range str {
x := int(c - '0')
if x < 0 || 9 < x {
return
}
numbers[i] = float64(x)
}
isValid = true
return
}
func operatorPairIterator(yield func(OperatorPair) bool) {
//ホントはNumbersLength依存にしたいけど再帰使わないロジックが思いつかなかったので妥協
for _, o1 := range operators {
for _, o2 := range operators {
for _, o3 := range operators {
if (!yield(OperatorPair{o1, o2, o3})) {
return
}
}
}
}
}
func calcAllPatterns(numbers *Numbers) []*Formula {
wg := new(sync.WaitGroup)
length := powInt(len(operators), NumbersLength-1)
fmt.Printf("length = %d\n", length)
ch := make(chan *Formula, length)
for op := range operatorPairIterator {
wg.Add(1)
go func(c chan *Formula) {
defer wg.Done()
f := newFormula(numbers, op)
c <- f
}(ch)
}
fmt.Println("waiting...")
wg.Wait()
close(ch)
fmt.Println("go")
formulas := make([]*Formula, 0, length)
for f := range ch {
formulas = append(formulas, f)
}
return formulas
}
func equalFloat64(x float64, y float64) bool {
//動作確認上では単純比較で問題なかったけど一応
const epsilon = 1e-9
abs := math.Abs(x - y)
return abs < epsilon
}
func main() {
router := gin.Default()
router.GET("/ping", func(c *gin.Context) {
str := c.Query("number")
isValid, numbers := str2Numbers(str)
if !isValid {
c.JSON(http.StatusBadRequest, gin.H{
"message": "これじゃあダメですね",
})
return
}
formulas := calcAllPatterns(&numbers)
answers := make([]string, 0, len(formulas))
for _, f := range formulas {
isTargetValue := equalFloat64(f.value, TargetAnswer)
if isTargetValue {
answers = append(answers, f.String())
}
}
c.JSON(http.StatusOK, gin.H{
"input": str,
"answers": answers,
})
})
router.Run()
}
色々定数とってますが、再帰なしで桁数に依存しない演算子の総当たりロジックが思いつかなかったので結局4桁固定。
ダサいけど本筋ではないし、LLMに考えてもらうのもあんま勉強にならないので妥協。
感想
そんな感じであんまり詰めきれてはないですが、とりあえず最低限の基礎は触れた気がします。多分おそらくきっと。
ともあれ触ってみる前後の認識の違いは以下の通りです。
- 型付けコンパイル
- なんかマルチスレッドに強いらしい
- 学習コストが低いらしい
一般動詞まんまで名前つけたやつ頭おかしいんじゃねえの?GoumanのGoか?
そして触ってみた後の認識やら感想やら。
- 思ったよりも低レイヤー
- なのに配列が値型なのか
- スライスがあるからいいのか天才か?
- いうほど学習コスト低いか?(ポインタとかゴリゴリに使うしなあ)
- gotoあるのか。興奮してきたな
- defer!お前を待っていたんだ!全言語に配ってくれ!
- goroutineとチャネル書きやすいんだか書きにくいんだか
- x := v の:を毎回忘れて怒られる
- constexprがあったら最高だったな
トップレベルの宣言で強制的に空行挟まれるの地味に腹立つな- イテレータのbool返却がなんなのか理解するのに5分くらいかかった
大雑把な感想としては「これが必要になるようなレベル高い現場入ってみたいなあ」という感じです。
内部の動きはあまり理解していない(例えばgoroutineのオーバーヘッド等)ので仔細は語れませんが、ちらっと触った程度では書きやすくて理解しやすかった部類。ポインタ最高!
何か機会があればねじ込んでみようかな。多分そんな機会ありませんが。
あとrustの方が面白かったのとC#が恋しくなってきた
余談
win10がサポート終了後、11の入らない化石PCに突っ込んだubuntu上で触ってました。
Goインストールしようとしたんですけど何かバージョンかなんかの問題でちょっと苦戦したので、面倒になってdocker上で動かしてました。
dockerの扱いが簡単なので開発はlinuxの方が好き。
メインPCにdocker desktop入れるの何故か気が進まないんですよね。なんでですかね(他人事)。