Go: Goの実践:CLIツールの開発 — フラグの解析、Cobraのサブコマンド、パスワードマネージャー
最終更新:2026-08-26
CLIツールの構築はGoの強みのひとつです。単一のバイナリにコンパイルされ、クロスプラットフォームに対応しており、依存関係も一切ありません。GoでCLIを作成することは、クラウドネイティブ時代において標準となっています。
docker から kubectl、そして gh に至るまで、主流の CLI ツールのほぼすべてが Go 言語で記述されています。このレッスンでは、CLI 開発のコアスキルをすべて習得し、完全なパスワードマネージャーを構築します。
1. 学習内容
flagパッケージは、コマンドライン引数を解析しますos.Args未処理の引数へのアクセスos.Stdin/os.Stdout標準ストリーム- コブラライブラリとサブコマンドパターン
- CLIとファイルI/Oの統合
- プロジェクト:パスワードマネージャー CLI(パスワードの生成・保存・取得・エクスポート)
2. 運用エンジニアの実話
(1) 課題:毎回手動でパスワードを生成しなければならないこと
ボブは運用エンジニアで、さまざまなサービス用に複雑なパスワードを頻繁に生成する必要があります:
「今日はPostgreSQL用のパスワードを生成し、明日はRedis用、明後日は管理者アカウント用。そのたびにパスワード生成サイトを開いて、1Passwordにコピー&ペーストしなければならない――1日に5回も。本当に面倒だ。それに、サイトごとに要件が異なるんだ。『12文字で特殊文字を含む』とか、『16文字で曖昧な文字を含まない』とか……」
ボブは、次のようなCLIツールを探しています:
# Desired CLI
$ passgen generate --length 16 --special
Generated: Kd9#mP2$xL7qR!vB
$ passgen save mydb -p Kd9#mP2$xL7qR!vB
Saved: mydb
$ passgen list
mydb (created: 2026-07-08)
admin (created: 2026-07-05)
api-key (created: 2026-07-01)
$ passgen get mydb
mydb: Kd9#mP2$xL7qR!vB
しかし、彼のニーズを完全に満たすツールは市場には存在しなかった。そこで彼は、Goを使って自分でツールを作成することにした。
(2) Goの解決策:flagパッケージ + ファイルI/O
// passgen.go — Version 1: Basic password generation
package main
import (
"crypto/rand"
"flag"
"fmt"
"math/big"
)
var (
length = flag.Int("length", 12, "password length")
special = flag.Bool("special", false, "include special characters")
count = flag.Int("count", 1, "number of passwords to generate")
)
const (
lowerChars = "abcdefghijklmnopqrstuvwxyz"
upperChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
digitChars = "0123456789"
specialChars = "!@#$%^&*()-_=+[]{}|;:,.<>?"
)
func generatePassword(length int, includeSpecial bool) string {
charset := lowerChars + upperChars + digitChars
if includeSpecial {
charset += specialChars
}
pwd := make([]byte, length)
for i := range pwd {
n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
pwd[i] = charset[n.Int64()]
}
return string(pwd)
}
func main() {
flag.Parse()
for i := 0; i < *count; i++ {
fmt.Println(generatePassword(*length, *special))
}
}
$ go run passgen.go --length 16 --special
Kd9#mP2$xL7qR!vB
$ go run passgen.go --length 8
aB3xK9mP
(3) パフォーマンス:マニュアル操作とCLIの比較
| 手法 | 速度 | 再現性 | CI/CD との連携 | セキュリティ |
|---|---|---|---|---|
| ウェブサイトの生成 | 1回あたり30秒 | ❌ | ❌ | ❌ (プライバシーリスク) |
| 手動で作成 | インスタンスあたり60秒 | ✅ 複製可能 | ❌ | ✅ |
| Go CLI | 1操作あたり0.5秒 | ✅ スクリプト化可能 | ✅ 統合可能 | ✅ ローカルで実行可能 |
go buildを実行した後、そのファイルをLinux、Mac、またはWindowsのサーバーにコピーするだけで実行できます。これが、CLIアプリケーションにGoを使用する最大の利点です。
3. flag パッケージ:コマンドライン引数の解析
(1) 旗の基本
package main
import (
"flag"
"fmt"
)
func main() {
// Register parameters (returns pointers)
name := flag.String("name", "World", "name to greet")
age := flag.Int("age", 0, "age")
verbose := flag.Bool("verbose", false, "verbose output")
// Parse (must be called before reading values)
flag.Parse()
if *verbose {
fmt.Printf("name=%s, age=%d\n", *name, *age)
}
fmt.Printf("Hello, %s!\n", *name)
}
$ go run main.go --name Alice --age 28 --verbose
name=Alice, age=28
Hello, Alice!
$ go run main.go
Hello, World!
(2) flag タイプのクイックリファレンス
| 関数 | 型 | 例 |
|---|---|---|
flag.String(name, default, usage) |
*string |
--name Alice |
flag.Int(name, default, usage) |
*int |
--port 8080 |
flag.Float64(name, default, usage) |
*float64 |
--ratio 0.5 |
flag.Bool(name, default, usage) |
*bool |
--verbose |
flag.Duration(name, default, usage) |
*time.Duration |
--timeout 5s |
▶ サンプル:フラグ付きパラメータとフラグなしパラメータ
package main
import (
"flag"
"fmt"
"os"
)
func main() {
// Custom Usage message
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: mytool [options] <file path>\n\n")
fmt.Fprintf(os.Stderr, "Options:\n")
flag.PrintDefaults()
}
// Register parameters
output := flag.String("o", "output.txt", "output file path")
limit := flag.Int("n", 0, "lines to process (0=all)")
debug := flag.Bool("debug", false, "enable debug mode")
// Parse
flag.Parse()
// Access non-flag arguments (remaining args)
args := flag.Args()
if len(args) < 1 {
flag.Usage()
os.Exit(1)
}
filePath := args[0]
fmt.Printf("Processing file: %s\n", filePath)
fmt.Printf("Output: %s\n", *output)
fmt.Printf("Lines: %d\n", *limit)
fmt.Printf("Debug: %v\n", *debug)
// Supports both -o and --o forms
// Supports both -o=file and -o file syntax
}
$ go run main.go -o result.txt -n 100 data.csv
Processing file: data.csv
Output: result.txt
Lines: 100
Debug: false
4. os.Args および os.Stdin/Stdout
(1) os.Args: 生の引数
package main
import (
"fmt"
"os"
)
func main() {
fmt.Printf("Program: %s\n", os.Args[0])
fmt.Printf("Arg count: %d\n", len(os.Args)-1)
for i, arg := range os.Args[1:] {
fmt.Printf("Arg %d: %s\n", i+1, arg)
}
}
$ go run main.go hello world --debug
Program: /tmp/main
Arg count: 3
Arg 1: hello
Arg 2: world
Arg 3: --debug
▶ サンプル:flag 変数のバインディング(IntVar / StringVar)
package main
import (
"flag"
"fmt"
)
func main() {
// flag.Int returns a pointer vs flag.IntVar binds to an existing variable
var (
name string
port int
debug bool
)
flag.StringVar(&name, "name", "World", "name")
flag.IntVar(&port, "port", 8080, "port")
flag.BoolVar(&debug, "debug", false, "debug mode")
flag.Parse()
fmt.Printf("name=%s, port=%d, debug=%v\n", name, port, debug)
}
flag.IntVar(&dest, ...) は既存の変数にバインドされますが、flag.Int(...) はポインタを返します。両者は完全に同等ですので、好みのスタイルを選んでください。IntVar は、一箇所で変数を宣言する場合に適しています。
(3) os.Stdin: パイプ入力
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func main() {
stat, _ := os.Stdin.Stat()
hasStdin := (stat.Mode() & os.ModeCharDevice) == 0
if hasStdin {
// Read from pipe (echo "hello" | go run main.go)
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
line := strings.ToUpper(scanner.Text())
fmt.Println(line)
}
} else {
// Read from arguments (go run main.go hello)
if len(os.Args) > 1 {
fmt.Println(strings.ToUpper(strings.Join(os.Args[1:], " ")))
} else {
fmt.Println("Please provide input (pipe or arguments)")
}
}
}
$ echo "hello world" | go run main.go
HELLO WORLD
$ go run main.go hello world
HELLO WORLD
▶ サンプル:対話型入力 (fmt.Scanf)
package main
import "fmt"
func main() {
var name string
var age int
fmt.Print("Enter name: ")
fmt.Scanf("%s", &name)
fmt.Print("Enter age: ")
fmt.Scanf("%d", &age)
fmt.Printf("Hello %s, you are %d years old\n", name, age)
}
▶ サンプル:os.Stdout のリダイレクト
package main
import (
"fmt"
"os"
)
func main() {
// Write directly to stdout
fmt.Fprintln(os.Stdout, "Standard output")
fmt.Fprintln(os.Stderr, "Error output")
// Redirect to file
file, _ := os.Create("output.log")
defer file.Close()
// fmt.Fprintln can write to any io.Writer
fmt.Fprintln(file, "This line was written to the file")
fmt.Fprintln(file, "Second line")
}
(6) 入力ソースの比較
| 入力方法 | コマンド例 | 使用例 |
|---|---|---|
| コマンドライン引数 | mytool --name Alice |
設定オプション、ファイル名 |
| stdin パイプ | cat data | mytool |
大量のデータ、連鎖処理 |
| 対話型入力 | mytool はユーザーからの入力を待機しています |
ガイダンスが必要な入力 |
| 環境変数 | MYTOOL_DEBUG=1 mytool |
機密設定 |
5. cobra ライブラリ:サブコマンドモード
(1) コブラの概要
cobraは、Go言語で最も人気のあるCLIフレームワーク(Kubernetesの開発者が作成)であり、サブコマンド、ヘルプメッセージ、オートコンプリートをサポートしています。
# Install cobra-cli
$ go install github.com/spf13/cobra-cli@latest
# Initialize project
$ cobra-cli init passgen
$ cobra-cli add generate
$ cobra-cli add save
$ cobra-cli add list
▶ サンプル:「cobra」サブコマンドの構造
// cmd/root.go
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "passgen",
Short: "Password Manager",
Long: `A CLI tool for generating, storing, and managing passwords.`,
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
// cmd/generate.go
package cmd
import (
"fmt"
"crypto/rand"
"math/big"
"github.com/spf13/cobra"
)
var (
genLength int
genSpecial bool
genCount int
)
var generateCmd = &cobra.Command{
Use: "generate",
Short: "Generate a random password",
Long: `Generate a random password with specified length and complexity.`,
Run: func(cmd *cobra.Command, args []string) {
for i := 0; i < genCount; i++ {
fmt.Println(generatePassword(genLength, genSpecial))
}
},
}
func init() {
rootCmd.AddCommand(generateCmd)
generateCmd.Flags().IntVarP(&genLength, "length", "l", 12, "password length")
generateCmd.Flags().BoolVarP(&genSpecial, "special", "s", false, "include special characters")
generateCmd.Flags().IntVarP(&genCount, "count", "c", 1, "number to generate")
}
func generatePassword(length int, special bool) string {
charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
if special {
charset += "!@#$%^&*()-_=+[]{}|;:,.<>?"
}
pwd := make([]byte, length)
for i := range pwd {
n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
pwd[i] = charset[n.Int64()]
}
return string(pwd)
}
// main.go
package main
import "passgen/cmd"
func main() {
cmd.Execute()
}
$ go build -o passgen
$ ./passgen generate --length 16 --special
Kd9#mP2$xL7qR!vB
$ ./passgen generate -l 8
aB3xK9mP
(3) フラッグ対コブラ
| ディメンション | フラグパッケージ | コブラ |
|---|---|---|
| 組み込み | ✅ 標準ライブラリ | ❌ サードパーティ製 |
| サブコマンド | ❌ 未対応 | ✅ ネイティブ対応 |
| 自動車のトラブルシューティング | ✅ 基礎知識 | ✅ 外観 |
| オートコンプリート | ❌ | ✅ bash/zsh/fish |
| ユースケース | 簡単なスクリプト | 複雑なマルチコマンドツール |
6. 完全な例:パスワードマネージャーのCLI
これらすべての知識を統合して、包括的なパスワードマネージャーを作りましょう:
// main.go
package main
import (
"bufio"
"crypto/rand"
"encoding/json"
"flag"
"fmt"
"math/big"
"os"
"strings"
"time"
)
// ---------- Data Structures ----------
type PasswordEntry struct {
Name string `json:"name"`
Password string `json:"password"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at,omitempty"`
}
type PasswordStore struct {
entries map[string]PasswordEntry
filePath string
}
const defaultStoreFile = "passwords.json"
// ---------- Storage Layer ----------
func NewPasswordStore(filePath string) *PasswordStore {
store := &PasswordStore{
entries: make(map[string]PasswordEntry),
filePath: filePath,
}
store.load()
return store
}
func (s *PasswordStore) load() {
data, err := os.ReadFile(s.filePath)
if err != nil {
return
}
json.Unmarshal(data, &s.entries)
}
func (s *PasswordStore) save() error {
data, err := json.MarshalIndent(s.entries, "", " ")
if err != nil {
return err
}
return os.WriteFile(s.filePath, data, 0600)
}
func (s *PasswordStore) Add(name, password string) {
now := time.Now()
if existing, ok := s.entries[name]; ok {
existing.Password = password
existing.UpdatedAt = now
s.entries[name] = existing
} else {
s.entries[name] = PasswordEntry{
Name: name,
Password: password,
CreatedAt: now,
}
}
s.save()
}
func (s *PasswordStore) Get(name string) (PasswordEntry, bool) {
entry, ok := s.entries[name]
return entry, ok
}
func (s *PasswordStore) Delete(name string) bool {
_, ok := s.entries[name]
if ok {
delete(s.entries, name)
s.save()
}
return ok
}
func (s *PasswordStore) List() []PasswordEntry {
result := make([]PasswordEntry, 0, len(s.entries))
for _, entry := range s.entries {
result = append(result, entry)
}
return result
}
// ---------- Password Generation ----------
const (
letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
digits = "0123456789"
specials = "!@#$%^&*()-_=+[]{}|;:,.<>?"
noAmbigu = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789"
)
func generatePassword(length int, useSpecial bool) (string, error) {
if length < 4 {
return "", fmt.Errorf("password length must be at least 4")
}
charset := letters + digits
if useSpecial {
charset += specials
}
pwd := make([]byte, length)
for i := range pwd {
n, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
if err != nil {
return "", err
}
pwd[i] = charset[n.Int64()]
}
return string(pwd), nil
}
// ---------- CLI Commands ----------
func cmdGenerate(args []string) {
cmd := flag.NewFlagSet("generate", flag.ExitOnError)
length := cmd.Int("length", 12, "password length")
special := cmd.Bool("special", false, "include special characters")
count := cmd.Int("count", 1, "number to generate")
_ = cmd.Bool("no-ambiguous", false, "exclude ambiguous characters (0OIl1)")
cmd.Parse(args)
for i := 0; i < *count; i++ {
pwd, err := generatePassword(*length, *special)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
fmt.Println(pwd)
}
}
func cmdSave(args []string, store *PasswordStore) {
cmd := flag.NewFlagSet("save", flag.ExitOnError)
password := cmd.String("password", "", "password (leave empty to auto-generate)")
length := cmd.Int("length", 16, "auto-generated password length")
cmd.Parse(args)
name := cmd.Arg(0)
if name == "" {
fmt.Fprintln(os.Stderr, "Usage: passgen save <name> [--password <pwd>]")
os.Exit(1)
}
pwd := *password
if pwd == "" {
var err error
pwd, err = generatePassword(*length, true)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
store.Add(name, pwd)
fmt.Printf("Saved: %s = %s\n", name, pwd)
}
func cmdGet(args []string, store *PasswordStore) {
if len(args) < 1 {
fmt.Fprintln(os.Stderr, "Usage: passgen get <name>")
os.Exit(1)
}
entry, ok := store.Get(args[0])
if !ok {
fmt.Fprintf(os.Stderr, "Not found: %s\n", args[0])
os.Exit(1)
}
fmt.Printf("%s: %s\n", entry.Name, entry.Password)
}
func cmdList(args []string, store *PasswordStore) {
entries := store.List()
if len(entries) == 0 {
fmt.Println("(no saved passwords)")
return
}
fmt.Printf("%-20s %-20s %s\n", "Name", "Password", "Created")
fmt.Println(strings.Repeat("-", 60))
for _, e := range entries {
pwd := e.Password
if len(pwd) > 16 {
pwd = pwd[:16] + "..."
}
fmt.Printf("%-20s %-20s %s\n",
e.Name, pwd, e.CreatedAt.Format("2006-01-02"))
}
}
func cmdDelete(args []string, store *PasswordStore) {
if len(args) < 1 {
fmt.Fprintln(os.Stderr, "Usage: passgen delete <name>")
os.Exit(1)
}
if store.Delete(args[0]) {
fmt.Printf("Deleted: %s\n", args[0])
} else {
fmt.Fprintf(os.Stderr, "Not found: %s\n", args[0])
}
}
func cmdExport(args []string, store *PasswordStore) {
output := "passwords_export.json"
if len(args) > 0 {
output = args[0]
}
data, _ := json.MarshalIndent(store.List(), "", " ")
os.WriteFile(output, data, 0600)
fmt.Printf("Exported to: %s\n", output)
}
// ---------- Main Function ----------
func printUsage() {
fmt.Println(`Password Manager CLI
Usage:
passgen <command> [options]
Commands:
generate Generate a random password
save Save a password
get Retrieve a password
list List all passwords
delete Delete a password
export Export passwords to a file
Options:
--store Specify storage file path (default: passwords.json)
Examples:
passgen generate --length 16 --special
passgen save mydb --length 20
passgen get mydb
passgen list
passgen delete mydb`)
}
func main() {
// Global --store parameter
storeFile := defaultStoreFile
if len(os.Args) > 1 {
for i := 1; i < len(os.Args); i++ {
if os.Args[i] == "--store" && i+1 < len(os.Args) {
storeFile = os.Args[i+1]
// Remove the store parameter
os.Args = append(os.Args[:i], os.Args[i+2:]...)
break
}
}
}
store := NewPasswordStore(storeFile)
if len(os.Args) < 2 {
printUsage()
return
}
command := os.Args[1]
args := os.Args[2:]
switch command {
case "generate":
cmdGenerate(args)
case "save":
cmdSave(args, store)
case "get":
cmdGet(args, store)
case "list":
cmdList(args, store)
case "delete":
cmdDelete(args, store)
case "export":
cmdExport(args, store)
case "help", "--help", "-h":
printUsage()
default:
fmt.Fprintf(os.Stderr, "Unknown command: %s\n", command)
fmt.Fprintf(os.Stderr, "Run 'passgen help' to see available commands\n")
os.Exit(1)
}
}
使用目的:
# Generate password
$ go run main.go generate --length 16 --special
Kd9#mP2$xL7qR!vB
# Save password (auto-generated)
$ go run main.go save mydb --length 20
Saved: mydb = aB3xK9mP$L7qR!vD2nFg
# Save password (manually specified)
$ go run main.go save admin --password "MyStr0ng!Pass"
Saved: admin = MyStr0ng!Pass
# Retrieve password
$ go run main.go get mydb
mydb: aB3xK9mP$L7qR!vD2nFg
# List
$ go run main.go list
Name Password Created
------------------------------------------------------------
mydb aB3xK9mP$L7qR!v... 2026-07-08
admin MyStr0ng!Pass 2026-07-08
# Export
$ go run main.go export
Exported to: passwords_export.json
# Specify storage file
$ go run main.go --store vault.json generate -l 10
flowchart TD
A[passgen command] --> B{switch command}
B -->|generate| C[flag.NewFlagSet parse<br/>--length --special --count]
B -->|save| D[Parse name + --password<br/>Auto-generate or specify password]
B -->|get| E[Look up by name from map]
B -->|list| F[Iterate map, output table]
B -->|delete| G[delete(map, name)]
B -->|export| H[json.Marshal + WriteFile]
C --> I[rand.Int secure random]
D --> J[store.Add → json → save]
E --> K[store.Get → output]
F --> L[store.List → format]
G --> M[store.Delete → json → save]
H --> N[store.List → json.Marshal]
flag.NewFlagSet はサブコマンドに使用され、各サブコマンドには独自のフラグセットがあります。flag.FlagSet.Parse は自身の引数のみを解析し、os.Args を変更することはありません。メインコマンドは、os.Args を使用して手動で値を割り当てる必要があります。
❓ よくある質問
flag と cobra のどちらを選べばいいですか?flag;複数コマンドのツール(gitスタイル:tool <command> [options])→ cobra。flagは標準ライブラリの一部ですが、cobraはより多くの機能を提供する一方で、依存関係の追加が必要となります。os.Args[1] を使用してコマンドを識別し、flag.NewFlagSet("sub", ...) を使用してサブコマンドの引数を解析します。Cobra での方法:rootCmd.AddCommand(subCmd) を使用すると、フレームワークが自動的に処理します。os.Args をどのように反復処理すればよいですか?os.Args[0] はプログラムのパス、os.Args[1:] は引数です。これらを反復処理するには、os.Args[i] または range os.Args[1:] を使用します。~/.config/passgen.json)への保存、(2) --storeパラメータを使用したパスの指定、(3) 環境変数(PASSGEN_STOREなど)。go build -o mytoolでコンパイルします;(2) GitHub Releasesにアップロードします;(3) go install github.com/user/mytool@latestを使用してインストールします。Goの静的バイナリを使えば、公開作業が非常に簡単になります。math/rand の代わりに crypto/rand を使用してください。—crypto/rand は、オペレーティングシステムの CSPRNG(/dev/urandom など)を使用しており、予測不可能です。math/rand は擬似乱数であり、暗号用途には適していません。bufio.NewScanner(os.Stdin) を使用すると行単位で読み取ることができ、fmt.Scanf を使用するとフォーマットされた入力を読み取ることができます。本番環境向けのツールには、gopass ライブラリ(パスワードを表示しない)の使用をお勧めします。mytool 2>/dev/null がサイレントモードで実行されるように、エラーメッセージは os.Stderr (os.Stdout ではなく)に出力する必要があります。fmt.Fprintln(os.Stderr, "error") を使用してください。📖 まとめ
flagパッケージは、コマンドライン引数を解析します:flag.String/flag.Int/flag.Boolos.Argsは生の引数にアクセスできるため、柔軟な手動での解析が可能になりますos.Stdin/os.Stdout/os.Stderr標準 I/O ストリームflag.NewFlagSetは、サブコマンド形式の CLI を実装しています- cobra ライブラリは、サブコマンドのための包括的なフレームワークを提供しています
crypto/randは、暗号学的に安全な乱数を生成します- CLI ツールのデータ永続化:JSON ファイル + メモリ内マップキャッシュ
go buildは、クロスプラットフォームでの配布用に単一のバイナリにコンパイルされます
📝 練習問題
-
基本問題(難易度 ⭐):
flagパッケージを使用して、wcのクローンを作成してください。mywcは、--lines、--words、--charsの 3 つのフラグをサポートし、標準入力またはファイルから入力を読み取り、統計情報を出力します。 -
上級演習(難易度 ⭐⭐):このレッスンで取り上げるパスワードマネージャーに、
--store環境変数のサポートを追加してください。PASSGEN_STORE環境変数が設定されている場合は、まずそれを保存パスとして使用し、設定されていない場合は--storeパラメータを使用し、最後にデフォルトのパスを使用するようにしてください。 -
課題(難易度:⭐⭐⭐):パスワードマネージャーに
statsコマンドを追加する。パスワードの総数、最長および最短のパスワード、最も頻繁に使用されている文字、および作成日時(今日/今週/今月/それ以前)ごとに分類した統計情報を集計する。sort.Slice+strings.Count+time.Timeを必ず使用する必要があります。