この記事は、DMM グループ Advent Calendar 2024の 20 日目の記事です。 こんにちは。データ基盤開発部のCDP(Customer Data Platform)グループで、DMMサービス横断顧客データシステムの開発運用保守を担当している渋谷です。 皆さんはシステムのドキュメントをどのようにメンテナンスされていらっしゃいますでしょうか。 設計時には頻繁に更新していたドキュメントも、開発が進んで実装段階に入った後の仕様変更はコードに反映させるのに手一杯で、ドキュメントは更新されなくなっていく…。 私が経験してきたプロジェクトではそんな光景がよく見られました。 ですがドキュメントが無価値という訳ではなく、特に判定などのビジネスロジックは非エンジニアのステークホルダーとのコミュニケーション時に使用する場面も多いです。 ビジネスロジックのドキュメント化が適切に行われるようになれば、プロジェクトの説明も理解もより進み、結果的に開発速度の向上に繋がっていきます。 本記事では、 Go のビジネスロジックコードから Mermaid 式でフローチャートを生成するコードの解説および作成の流れを説明することを目的とします。 なおこの記事は、DMM.go #9 での発表を元にしています。 Mermaid は、Markdown ライクなテキスト定義を使用して図やチャートを動的に作成・編集できる JavaScript ベースのツールです。 作成できるダイアグラムは、フローチャートやシーケンス図、ガントチャートなどの多岐に渡ります。 ブラウザで簡単に利用可能で、GitHub の README や静的サイトジェネレーターにも統合できます。 AST(Abstract Syntax Tree、抽象構文木)は、ソースコードの構造を階層的に表現するデータ構造です。 コードを解析して得られるこの木構造は、プログラムの構文や構造を抽象的に示し、コンパイラや静的解析ツール、コード変換ツールなどで広く利用されています。 AST を活用することで、コードのリファクタリング、自動生成、エラー検出などが効率的に行えるようになります。 早速、Go のビジネスロジックから Mermaid 式を出力するコードの現時点での完成版をご覧ください。 ビジネスロジックのドキュメント化を行うにあたり、まず Mermaid 式フローチャートを書くことを考えました。 最初は手で書き始めたのですが、その面倒さにすぐにギブアップしました。 次に弊社グループの生成AIを使って、ビジネスロジックのコードからMermaidを書くことを試みましたが、期待したMermaid式がなかなか生成できませんでした。 また、同じ苦労をロジックの数だけ繰り返すのもしんどいな…というのが正直なところでした。 上司にもアドバイスをいただいて既存の仕組みなども調べましたが、 Go 言語における静的解析では skelton や analyzer などが主で、ビジネスロジックの流れを掴むという目的にはちょっと合わないな、と判断しました。 どうしたものかと悩んでいたときに、不意に過去のDMM.goの発表を思い出したのです。 DMM.go #7 で弊社の n9te9 さんが「単体テストのスケルトンコードを自動生成するツールを作った話」を発表されました。 私はこの発表で、AST (抽象構文木) で Go のコードを構文として解釈して操作できることを知りました。 生成 AI でビジネスロジックから Mermaid 式を出力してもらうのが難しいのはすでに分かっていました。 であれば、生成 AI に書いてもらうのは、ビジネスロジックの解析から Mermaid 式を出力するコードの方が精度はいいかも?と思いついたのです。 早速、弊社グループの生成 AI で試してみたのですが、そうは問屋が卸しませんでした。 なかなかいい感じに Mermaid 式を組み立てるコードが生成できません。 ここで手詰まりか…と思ったのですが、当時発表されたばかりの ChatGPT o1-preview のことを思い出したのです。 ChatGPT o1-preview は 2024/09/12 にリリースされた、深い推論が得意な新しい学習モデルです。 ちょうどこのモデルが出たばかりの頃でしたので、社外に出しても問題ない形に抽象化したビジネスロジックを投げてみました。 あんなざっくりとしたプロンプトでここまでコードが出力されることに、正直驚嘆しました。 if 文の分岐など直す必要がある箇所は何箇所がありましたが、90%のコードはそのまま使えそうだ、と思えるものでした。 初期コードから改善したかったのは主に下記の四点でした。 このうち、1番と2番は ChatGPT に追加のプロンプトを投げて解決できました。 コードの意図を示すため、コード前後のコメントをMermaidに組み込みたかったのですが、こちらは ChatGPT だけでは解決できませんでした。 理由としては、コードを構文化した時にコメントがどのように解析されるのかの知見が私自身になく、的確な指示ができなかったためです。 そのため、泥臭くはありますが実際にコメントが出てくるコード前後をプリントデバッグして、コメントが存在しうる範囲を確認して組み込みました。 4番目の入出力処理は自分で書いた方が速かったのでそのまま書きました。 こうして出来上がったのが、最初に示したコードとその出力結果になります。 コードを AST で解析して Mermaid 式を出力するのは、すべてのコードにおいて有効、という訳ではありません。 例えば、DB や 外部API など、外部とのやり取りに関しては、手続きばかり記載されていて実際のコードでもフローチャートでも得られる情報量はさして変わらない…という結果になりました。 そのため、このコードで出力できる Mermaid 式の中で有益なのは主にビジネスロジックである、という結論に達しています。 今回のコードを現在は手動で実行していますが、将来的にはドキュメントとしての体裁を整えて CI に組み込み、PR をマージしたタイミングでビジネスロジックと同期したドキュメントを自動生成する予定です。 そして生成したドキュメントを用いて、非エンジニアや新規加入メンバーとのコミュニケーションに活用できるようにしていきたいと考えています。 Merry Christmas & Happy New Year !はじめに
この記事で伝えたいこと
解決したい課題
用語解説
Mermaid
AST
完成版コード
このコードで実現できること
このコードでまだ実現できないこと
コード
package main
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"log"
"os"
"strings"
)
func main() {
// ファイルを開く
file, err := os.Create("scripts/ast/mermaid.md")
if err != nil {
log.Fatal(err)
}
defer file.Close()
for _, targets := range [][]string{
{"usecase/member.go", "isMember"},
} {
path := targets[0]
method := targets[1]
// 入力Goコードを読み込む
src, err := os.ReadFile(path)
if err != nil {
log.Fatal(err)
}
outputMermaid(src, method, file)
}
}
// outputMermaid は指定された関数を AST で解析してコードの内容を Mermaid 式で出力します
func outputMermaid(src []byte, method string, file *os.File) {
// ファイルセットを作成
fset := token.NewFileSet()
// ASTを生成
f, err := parser.ParseFile(fset, "", src, parser.ParseComments)
if err != nil {
log.Fatal(err)
}
// ノードとエッジを格納するためのスライス
var nodes []string
var edges []string
var doc string
// エッジの重複を避けるためのマップ
edgeSet := make(map[string]bool)
// ノードIDを生成するためのカウンタ
nodeCounter := 0
genNodeID := func() string {
nodeCounter++
return fmt.Sprintf("N%d", nodeCounter)
}
// ASTを走査
ast.Inspect(f, func(n ast.Node) bool {
switch fn := n.(type) {
case *ast.FuncDecl:
if fn.Name.Name == method {
startID := genNodeID()
structName := ""
if fn.Recv != nil && len(fn.Recv.List) > 0 {
recvType := fn.Recv.List[0].Type
switch t := recvType.(type) {
case *ast.Ident:
structName = fmt.Sprintf("%s.", t.Name)
case *ast.StarExpr:
if ident, ok := t.X.(*ast.Ident); ok {
structName = fmt.Sprintf("%s.", ident.Name)
}
}
}
doc = fn.Doc.Text()
exprStr := fmt.Sprintf("%s([\"`**%s%s**`\"])", startID, structName, fn.Name.Name)
nodes = append(nodes, exprStr)
parseBlockStmt(f, fset, fn.Body, startID, &nodes, &edges, edgeSet, genNodeID, false)
}
}
return true
})
// Mermaid形式でmdファイルに出力
outputMermaidFormat(nodes, edges, doc, file)
}
func outputMermaidFormat(nodes []string, edges []string, doc string, file *os.File) {
file.WriteString(fmt.Sprintf("## %s\n\n", doc))
// ノードを書き込む
file.WriteString("```\n")
file.WriteString("flowchart TD\n")
for _, node := range nodes {
file.WriteString(fmt.Sprintf(" %s\n", node))
}
for _, edge := range edges {
file.WriteString(fmt.Sprintf(" %s\n", edge))
}
file.WriteString("```\n\n\n")
}
func parseBlockStmt(
f *ast.File,
fset *token.FileSet,
block *ast.BlockStmt,
currentID string,
nodes *[]string,
edges *[]string,
edgeSet map[string]bool,
genNodeID func() string,
suppressInitialEdge bool,
) string {
for _, stmt := range block.List {
// コメントの取得
comments := getComments(f, fset, stmt)
commentStr := strings.Join(comments, "\\n")
switch s := stmt.(type) {
case *ast.AssignStmt, *ast.ExprStmt, *ast.ReturnStmt:
nodeID := genNodeID()
var label string
switch st := s.(type) {
case *ast.AssignStmt:
exprStr := stmtToString(st)
if commentStr != "" {
exprStr = commentStr + "\\n" + exprStr
}
label = escapeString(exprStr)
*nodes = append(*nodes, fmt.Sprintf("%s[\"%s\"]", nodeID, label))
case *ast.ExprStmt:
exprStr := exprToString(st.X)
if commentStr != "" {
exprStr = commentStr + "\\n" + exprStr
}
label = escapeString(exprStr)
*nodes = append(*nodes, fmt.Sprintf("%s[\"%s\"]", nodeID, label))
case *ast.ReturnStmt:
exprStr := stmtToString(st)
if commentStr != "" {
exprStr = commentStr + "\\n" + exprStr
}
label = escapeString(exprStr)
*nodes = append(*nodes, fmt.Sprintf("%s([\"%s\"])", nodeID, label))
}
if !suppressInitialEdge {
addEdge(currentID, nodeID, edges, edgeSet)
}
suppressInitialEdge = false
currentID = nodeID
case *ast.IfStmt:
// if 文
cond := exprToString(s.Cond)
if commentStr != "" {
cond = commentStr + "\\n" + cond
}
condID := genNodeID()
label := escapeString(cond)
*nodes = append(*nodes, fmt.Sprintf("%s{{\"%s\"}}", condID, label))
if !suppressInitialEdge {
addEdge(currentID, condID, edges, edgeSet)
}
suppressInitialEdge = false
// "Yes" 分岐
yesID := parseBlockStmt(f, fset, s.Body, condID, nodes, edges, edgeSet, genNodeID, true)
addLabeledEdge(condID, "Yes", yesID, edges, edgeSet)
// "No" 分岐
if s.Else != nil {
elseID := parseElseStmt(f, fset, s.Else, condID, nodes, edges, edgeSet, genNodeID)
addLabeledEdge(condID, "No", elseID, edges, edgeSet)
currentID = elseID
} else {
noID := genNodeID()
*nodes = append(*nodes, fmt.Sprintf("%s[\"処理続行\"]", noID))
addLabeledEdge(condID, "No", noID, edges, edgeSet)
currentID = noID
}
case *ast.RangeStmt:
// for文
key := exprToString(s.Key)
value := exprToString(s.Value)
x := exprToString(s.X)
rangeLabel := fmt.Sprintf("for %s, %s := range %s", key, value, x)
if commentStr != "" {
rangeLabel = commentStr + "\\n" + rangeLabel
}
rangeID := genNodeID()
label := escapeString(rangeLabel)
*nodes = append(*nodes, fmt.Sprintf("%s{{\"%s\"}}", rangeID, label))
if !suppressInitialEdge {
addEdge(currentID, rangeID, edges, edgeSet)
}
suppressInitialEdge = false
// サブグラフの開始
subgraphID := genNodeID()
*nodes = append(*nodes, fmt.Sprintf("subgraph %s [\"%s\"]", subgraphID, label))
// ループ本体
bodyID := parseBlockStmt(f, fset, s.Body, rangeID, nodes, edges, edgeSet, genNodeID, true)
addLabeledEdge(rangeID, "Body", bodyID, edges, edgeSet)
// サブグラフの終了
*nodes = append(*nodes, "end")
// ループの継続
addEdge(bodyID, rangeID, edges, edgeSet)
currentID = rangeID
}
}
return currentID
}
func parseElseStmt(
f *ast.File,
fset *token.FileSet,
elseStmt ast.Stmt,
parentID string,
nodes *[]string,
edges *[]string,
edgeSet map[string]bool,
genNodeID func() string,
) string {
switch stmt := elseStmt.(type) {
case *ast.BlockStmt:
return parseBlockStmt(f, fset, stmt, parentID, nodes, edges, edgeSet, genNodeID, true)
case *ast.IfStmt:
cond := exprToString(stmt.Cond)
comments := getComments(f, fset, stmt)
if len(comments) > 0 {
cond = strings.Join(comments, "\\n") + "\\n" + cond
}
condID := genNodeID()
label := escapeString(cond)
*nodes = append(*nodes, fmt.Sprintf("%s{{\"%s\"}}", condID, label))
addEdge(parentID, condID, edges, edgeSet)
// "Yes" 分岐
yesID := parseBlockStmt(f, fset, stmt.Body, condID, nodes, edges, edgeSet, genNodeID, true)
addLabeledEdge(condID, "Yes", yesID, edges, edgeSet)
// "No" 分岐
if stmt.Else != nil {
noID := parseElseStmt(f, fset, stmt.Else, condID, nodes, edges, edgeSet, genNodeID)
addLabeledEdge(condID, "No", noID, edges, edgeSet)
return noID
}
return condID
default:
elseID := genNodeID()
stmtStr := stmtToString(stmt)
comments := getComments(f, fset, stmt)
if len(comments) > 0 {
stmtStr = strings.Join(comments, "\\n") + "\\n" + stmtStr
}
label := escapeString(stmtStr)
*nodes = append(*nodes, fmt.Sprintf("%s[\"%s\"]", elseID, label))
addEdge(parentID, elseID, edges, edgeSet)
return elseID
}
}
func exprToString(expr ast.Expr) string {
switch e := expr.(type) {
case *ast.BinaryExpr:
return exprToString(e.X) + " " + e.Op.String() + " " + exprToString(e.Y)
case *ast.Ident:
return e.Name
case *ast.BasicLit:
return e.Value
case *ast.CallExpr:
return exprToString(e.Fun) + "(" + argsToString(e.Args) + ")"
case *ast.SelectorExpr:
return exprToString(e.X) + "." + e.Sel.Name
case *ast.StarExpr:
return "*" + exprToString(e.X)
case *ast.UnaryExpr:
return e.Op.String() + exprToString(e.X)
case *ast.ParenExpr:
return "(" + exprToString(e.X) + ")"
default:
return ""
}
}
func argsToString(args []ast.Expr) string {
var argStrs []string
for _, arg := range args {
argStrs = append(argStrs, exprToString(arg))
}
return strings.Join(argStrs, ", ")
}
func stmtToString(stmt ast.Stmt) string {
switch s := stmt.(type) {
case *ast.AssignStmt:
var lhs []string
for _, expr := range s.Lhs {
lhs = append(lhs, exprToString(expr))
}
var rhs []string
for _, expr := range s.Rhs {
rhs = append(rhs, exprToString(expr))
}
return fmt.Sprintf("%s %s %s", strings.Join(lhs, ", "), s.Tok.String(), strings.Join(rhs, ", "))
case *ast.ReturnStmt:
var results []string
for _, expr := range s.Results {
results = append(results, exprToString(expr))
}
return "return " + strings.Join(results, ", ")
default:
return ""
}
}
func escapeString(s string) string {
// ダブルクォーテーションを #quot; に置換
s = strings.ReplaceAll(s, `"`, "#quot;")
// 特殊文字をエスケープ
s = strings.ReplaceAll(s, "{", "\\{")
s = strings.ReplaceAll(s, "}", "\\}")
s = strings.ReplaceAll(s, "<", "\\<")
s = strings.ReplaceAll(s, ">", "\\>")
return s
}
func addEdge(from, to string, edges *[]string, edgeSet map[string]bool) {
edge := fmt.Sprintf(" %s --> %s", from, to)
if !edgeSet[edge] {
*edges = append(*edges, edge)
edgeSet[edge] = true
}
}
func addLabeledEdge(from, label, to string, edges *[]string, edgeSet map[string]bool) {
edge := fmt.Sprintf(" %s --> |%s| %s", from, label, to)
if !edgeSet[edge] {
*edges = append(*edges, edge)
edgeSet[edge] = true
}
}
func getComments(astFile *ast.File, fset *token.FileSet, node ast.Node) []string {
var comments []string
if node == nil {
return comments
}
pos := node.Pos()
end := node.End()
file := fset.File(pos)
if file == nil {
return comments
}
// ノードの直前および直後のコメントを取得
for _, cgroup := range astFile.Comments {
if cgroup.Pos() <= pos && cgroup.End() <= end && cgroup.End()+3 >= pos {
for _, comment := range cgroup.List {
text := strings.TrimSpace(strings.TrimPrefix(comment.Text, "//"))
comments = append(comments, text)
}
}
}
return comments
}
工夫箇所
コメント
if 文
for 文
出力結果 (if 文)
元となるビジネスロジック
// 会員IDの取得
func (u *UseCase) getMemberId(ctx context.Context, sessionId, loginId string) (string, error) {
memberId, err := u.getMemberIdBySessionId(ctx, sessionId)
if err == nil && memberId != "" {
// セッションIDから会員IDが取得できた場合は、それを利用する
return memberId, nil
}
// 取れなかった場合のみログインIDから会員IDを取得する
memberId, err = u.getMemberIdByLoginId(ctx, loginId)
if err != nil {
return "", err
}
return memberId, nil
}
出力された Mermaid
flowchart TD
N1(["`**UseCase.getMemberId**`"])
N2["memberId, err := u.getMemberIdBySessionId(ctx, sessionId)"]
N3{{"err == nil && memberId != #quot;#quot;"}}
N4(["セッションIDから会員IDが取得できた場合は、それを利用する\nreturn memberId, nil"])
N5["処理続行"]
N6["取れなかった場合のみログインIDから会員IDを取得する\nmemberId, err = u.getMemberIdByLoginId(ctx, loginId)"]
N7{{"err != nil"}}
N8(["return #quot;#quot;, err"])
N9["処理続行"]
N10(["return memberId, nil"])
N1 --> N2
N2 --> N3
N3 --> |Yes| N4
N3 --> |No| N5
N5 --> N6
N6 --> N7
N7 --> |Yes| N8
N7 --> |No| N9
N9 --> N10
出力結果 (for 文)
元となるビジネスロジック
func (u *UseCase) getPurchasesByMemberIds(ctx context.Context, memberIds []string) ([][]string, error) {
var purchases [][]string
for _, memberId := range memberIds {
// 会員IDごとに購入情報を取得
purchase, err := u.getPurchaseByMemberId(ctx, memberId)
if err != nil {
// エラーが発生した場合はnilを返す
return nil, err
}
// 購入情報を追加
purchases = append(purchases, purchase)
}
return purchases, nil
}
出力された Mermaid
flowchart TD
N1(["`**UseCase.getPurchasesByMemberIds**`"])
N2{{"for _, memberId := range memberIds"}}
subgraph N3 ["for _, memberId := range memberIds"]
N4["会員IDごとに購入情報を取得\npurchase, err := u.getPurchaseByMemberId(ctx, memberId)"]
N5{{"err != nil"}}
N6(["return nil, err"])
N7["処理続行"]
N8["購入情報を追加\npurchases = append(purchases, purchase)"]
end
N9(["return purchases, nil"])
N1 --> N2
N4 --> N5
N5 --> |Yes| N6
N5 --> |No| N7
N7 --> N8
N2 --> |Body| N8
N8 --> N2
N2 --> N9
コード完成までの道のり
生成 AI で Mermaid 式を出力する
AST の存在を思い出す
ChatGPT o1-preview を触ってみる
プロンプト
goのASTパッケージを利用して、入力したロジックコードを解析して、ロジックのフローチャートをmermaid式で出力するgoコードを書きたいです。
ステップバイステップで落ち着いて考えてください
## 入力コード例
// 会員IDの取得
func (u *UseCase) getMemberId(ctx context.Context, sessionId, loginId string) (string, error) {
memberId, err := u.getMemberIdBySessionId(ctx, sessionId)
if err == nil && memberId != "" {
// セッションIDから会員IDが取得できた場合は、それを利用する
return memberId, nil
}
// 取れなかった場合のみログインIDから会員IDを取得する
memberId, err = u.getMemberIdByLoginId(ctx, loginId)
if err != nil {
return "", err
}
return memberId, nil
}
## 出力mermaid例
flowchart TD
A[Start]
B[getMemberIdBySessionId]
C{MemberId\nis not empty?}
D[Return MemberId]
E[getMemberIdByLoginId]
F[Return MemberId]
G[Error\nReturn empty]
A --> B
B --> C
C --> |Yes| D
C --> |No| E
D --> G
E --> F
F --> G
出力されたコード
package main
import (
"go/parser"
"go/token"
"io/ioutil"
"log"
)
func main() {
// 入力Goコードを読み込む
src, err := ioutil.ReadFile("input.go")
if err != nil {
log.Fatal(err)
}
// ファイルセットを作成
fset := token.NewFileSet()
// ASTを生成
f, err := parser.ParseFile(fset, "", src, parser.ParseComments)
if err != nil {
log.Fatal(err)
}
// ノードとエッジを格納するためのスライス
var nodes []string
var edges []string
// ノードIDを生成するためのカウンタ
nodeCounter := 0
genNodeID := func() string {
nodeCounter++
return fmt.Sprintf("N%d", nodeCounter)
}
// ASTを走査
ast.Inspect(f, func(n ast.Node) bool {
switch fn := n.(type) {
case *ast.FuncDecl:
if fn.Name.Name == "getMemberId" {
startID := genNodeID()
nodes = append(nodes, fmt.Sprintf("%s[Start]", startID))
parseBlockStmt(fn.Body, startID, &nodes, &edges, genNodeID)
}
}
return true
})
// Mermaid形式で出力
fmt.Println("flowchart TD")
for _, node := range nodes {
fmt.Printf(" %s\n", node)
}
for _, edge := range edges {
fmt.Printf(" %s\n", edge)
}
}
func parseBlockStmt(block *ast.BlockStmt, currentID string, nodes *[]string, edges *[]string, genNodeID func() string) string {
for _, stmt := range block.List {
switch s := stmt.(type) {
case *ast.AssignStmt:
// 代入文の処理(必要に応じて)
case *ast.ExprStmt:
// 式の処理(関数呼び出しなど)
if call, ok := s.X.(*ast.CallExpr); ok {
funcName := exprToString(call.Fun)
nodeID := genNodeID()
*nodes = append(*nodes, fmt.Sprintf("%s[%s]", nodeID, funcName))
*edges = append(*edges, fmt.Sprintf(" %s --> %s", currentID, nodeID))
currentID = nodeID
}
case *ast.IfStmt:
cond := exprToString(s.Cond)
condID := genNodeID()
*nodes = append(*nodes, fmt.Sprintf("%s{%s}", condID, cond))
*edges = append(*edges, fmt.Sprintf(" %s --> %s", currentID, condID))
// "Yes"分岐
yesID := parseBlockStmt(s.Body, condID, nodes, edges, genNodeID)
*edges = append(*edges, fmt.Sprintf(" %s --> |Yes| %s", condID, yesID))
// "No"分岐
if s.Else != nil {
elseID := genNodeID()
*nodes = append(*nodes, fmt.Sprintf("%s[Else]", elseID))
*edges = append(*edges, fmt.Sprintf(" %s --> |No| %s", condID, elseID))
currentID = elseID
} else {
currentID = condID
}
case *ast.ReturnStmt:
retID := genNodeID()
*nodes = append(*nodes, fmt.Sprintf("%s[Return]", retID))
*edges = append(*edges, fmt.Sprintf(" %s --> %s", currentID, retID))
currentID = retID
}
}
return currentID
}
func exprToString(expr ast.Expr) string {
// 式を文字列に変換する簡易的な実装
switch e := expr.(type) {
case *ast.BinaryExpr:
return exprToString(e.X) + " " + e.Op.String() + " " + exprToString(e.Y)
case *ast.Ident:
return e.Name
case *ast.BasicLit:
return e.Value
case *ast.CallExpr:
return exprToString(e.Fun)
case *ast.SelectorExpr:
return exprToString(e.X) + "." + e.Sel.Name
default:
return ""
}
}
生成できた Mermaid 式
flowchart TD
N1[Start]
N2{err == nil && memberId != ""}
N3[Return]
N4{err != nil}
N5[Return]
N6[Return]
N1 --> N2
N2 --> N3
N2 --> |Yes| N3
N2 --> N4
N4 --> N5
N4 --> |Yes| N5
N4 --> N6
初回出力時の感想
ChatGPT o1-preview と真剣に向き合う
改善したかった点
1. 2. 解決後の Mermaid
flowchart TD
N1(["`**UseCase.getMemberId**`"])
N2["memberId, err := u.getMemberIdBySessionId(ctx, sessionId)"]
N3{{"err == nil && memberId != #quot;#quot;"}}
N4(["return memberId, nil"])
N5["処理続行"]
N6["memberId, err = u.getMemberIdByLoginId(ctx, loginId)"]
N7{{"err != nil"}}
N8(["return #quot;#quot;, err"])
N9["処理続行"]
N10(["return memberId, nil"])
N1 --> N2
N2 --> N3
N3 --> |Yes| N4
N3 --> |No| N5
N5 --> N6
N6 --> N7
N7 --> |Yes| N8
N7 --> |No| N9
N9 --> N10
コメントの出力
ファイルの入出力
このコードが苦手なこと
将来の展望