refactor(go)!: 分离练习区与独立项目区

- 将现有练习及 module 文件迁移至 go/practice,保持代码和依赖不变
- 新增 go/projects 项目区说明,更新目录约定与开发命令

BREAKING CHANGE: Go 练习命令需改在 go/practice 目录下执行
This commit is contained in:
2026-10-15 22:26:57 +08:00
parent 86f6eb39b2
commit d375463de5
21 changed files with 91 additions and 33 deletions
+149
View File
@@ -0,0 +1,149 @@
package crypto
import (
"errors"
"testing"
"time"
"golang.org/x/crypto/bcrypt"
)
var (
ErrInvalidPassword = errors.New("invalid password")
ErrInvalidHash = errors.New("invalid password hash")
)
type Hasher struct {
cost int // cost 可以理解成 bcrypt 的“计算难度”参数,cost 越高,计算 bcrypt hash 就越慢,攻击者暴力破解密码也越贵
}
func NewHasher(cost int) (*Hasher, error) {
if cost < bcrypt.MinCost || cost > bcrypt.MaxCost {
return nil, errors.New("invalid bcrypt cost")
}
return &Hasher{
cost: cost,
}, nil
}
// Hash 用于注册、重置密码、修改密码。
func (h *Hasher) Hash(password string) (string, error) {
if len(password) == 0 {
return "", ErrInvalidPassword
}
// bcrypt 只使用密码前 72 bytes。
// 生产环境建议显式拒绝,而不是静默截断。
if len([]byte(password)) > 72 {
return "", ErrInvalidPassword
}
hash, err := bcrypt.GenerateFromPassword(
[]byte(password),
h.cost,
)
if err != nil {
return "", err
}
return string(hash), nil
}
// Verify 用于登录时验证密码。
func (h *Hasher) Verify(password, encodedHash string) (bool, error) {
if len(password) == 0 || len([]byte(password)) > 72 {
return false, ErrInvalidPassword
}
if encodedHash == "" {
return false, ErrInvalidHash
}
err := bcrypt.CompareHashAndPassword(
[]byte(encodedHash),
[]byte(password),
)
switch {
case err == nil:
return true, nil
case errors.Is(err, bcrypt.ErrMismatchedHashAndPassword):
return false, nil
default:
// 例如数据库里的 hash 已损坏或格式不合法。
return false, ErrInvalidHash
}
}
// NeedsRehash 判断数据库中的 hash 是否使用了较旧的 cost。
func (h *Hasher) NeedsRehash(encodedHash string) bool {
cost, err := bcrypt.Cost([]byte(encodedHash))
if err != nil {
return true
}
return cost != h.cost
}
func TestBcrypt(t *testing.T) {
oldHasher, err := NewHasher(10)
if err != nil {
t.Fatal(err)
}
newHasher, err := NewHasher(12)
if err != nil {
t.Fatal(err)
}
password := "password"
oldHash, err := oldHasher.Hash(password)
if err != nil {
t.Fatal(err)
}
t.Log(oldHash)
newHash, err := newHasher.Hash(password)
if err != nil {
t.Fatal(err)
}
t.Log(newHash)
err = bcrypt.CompareHashAndPassword([]byte(oldHash), []byte(password))
if err != nil {
t.Fatal(err)
}
}
func TestHashPassword(t *testing.T) {
hasher, err := NewHasher(12)
if err != nil {
t.Fatal(err)
}
password := "password"
hash, err := hasher.Hash(password)
if err != nil {
t.Fatal(err)
}
t.Log(hash)
if ok, err := hasher.Verify(password, hash); err != nil || !ok {
t.Fatal("verify failed")
}
if ok := hasher.NeedsRehash(hash); !ok {
t.Fatal("needs rehash failed")
}
}
func TestGenerateTime(t *testing.T) {
start := time.Now()
_, err := bcrypt.GenerateFromPassword(
[]byte("TestPassword123!"),
14,
)
if err != nil {
t.Fatal(err)
}
t.Log(time.Since(start), err)
}
+34
View File
@@ -0,0 +1,34 @@
package error
import (
"errors"
"fmt"
"testing"
)
type MatchError struct {
code int
err error
}
func (e *MatchError) Error() string {
return fmt.Sprintf("code: %d, error: %v", e.code, e.err)
}
func GetMatchError() error {
return &MatchError{code: 1, err: errors.New("123")}
}
func GetError() error {
return errors.New("123")
}
func TestError(t *testing.T) {
if err, ok := errors.AsType[*MatchError](GetMatchError()); ok {
t.Log(err)
} else {
t.Errorf("expected MatchError, got %T", GetMatchError())
}
if err, ok := errors.AsType[*MatchError](GetError()); ok {
t.Log(err)
} else {
t.Errorf("expected MatchError, got %T", GetError())
}
}
+68
View File
@@ -0,0 +1,68 @@
package main
import (
"fmt"
"testing"
"time"
// 引入两个包进行对比
jsonV1 "encoding/json"
jsonV2 "encoding/json/v2"
)
func TestV2Null(t *testing.T) {
type Product struct {
Name string `json:"name"`
Tags []string `json:"tags"`
}
p := Product{
Name: "product1",
}
data1, _ := jsonV1.Marshal(p)
data2, _ := jsonV2.Marshal(p)
t.Log(string(data1))
t.Log(string(data2))
}
func TestOmitZeroVsEmpty(t *testing.T) {
// ===== V1 结构体:使用 omitempty =====
type EventV1 struct {
Title string `json:"title"`
CreatedAt time.Time `json:"created_at,omitempty"` // ⚠️ omitempty 对 time.Time 无效
}
// ===== V2 结构体:使用 omitzero =====
type EventV2 struct {
Title string `json:"title"`
CreatedAt time.Time `json:"created_at,omitzero"` // ✅ omitzero 能识别 IsZero()
}
// 都不给 CreatedAt 赋值,保持 Go 的零值 time.Time{}
// 1. 测试 v1 + omitempty
e1 := EventV1{Title: "Go Meetup"}
data1, _ := jsonV1.Marshal(e1)
fmt.Println("v1 + omitempty 输出:", string(data1))
// 2. 测试 v2 + omitzero
e2 := EventV2{Title: "Go Meetup"}
data2, _ := jsonV2.Marshal(e2)
fmt.Println("v2 + omitzero 输出:", string(data2))
}
func TestMultiStyle(t *testing.T) {
type MultiStyleUser struct {
// 1. 严格匹配:JSON key 必须精确是 "user_id"
ID string `json:"user_id,case:strict"`
// 2. 忽略大小写:JSON key 是 "Name"、"NAME"、"name" 都能成功赋值
Name string `json:"name,case:ignore"`
}
// 假设第三方传来的 JSON 风格混杂
jsonData := []byte(`{
"user_id": "1001",
"NAME": "Gopher"
}`)
var m MultiStyleUser
jsonV2.Unmarshal(jsonData, &m)
t.Log(m)
}
+18
View File
@@ -0,0 +1,18 @@
package net
import (
"net"
"testing"
)
func TestNet(t *testing.T) {
s := net.JoinHostPort("127.0.0.1", "8080")
t.Log(s)
t.Log(s)
addrs, err := net.LookupAddr("198.18.0.18")
if err != nil {
t.Fatal(err)
}
t.Log(addrs)
}
+109
View File
@@ -0,0 +1,109 @@
package testing
import (
"fmt"
"sort"
"testing"
"time"
)
func TestNormal(t *testing.T) {
numbers := map[int][]int{
1: {8, 2, 4, 4},
2: {0, 5, 3},
3: {1},
}
for _, numbers := range numbers {
middle := GetMiddNumber(numbers)
t.Log(middle)
}
}
func GetMiddNumber(numbers []int) float64 {
sort.Ints(numbers)
length := len(numbers)
isOddNUmber := length%2 == 1
if !isOddNUmber {
return float64(numbers[length/2-1]+numbers[length/2]) / 2.0
}
return float64(numbers[length/2])
}
func TestChannel(t *testing.T) {
var ball = make(chan string)
kickBall := func(playerName string) {
for {
t.Log(<-ball, "传球", "\n")
time.Sleep(time.Second)
ball <- playerName
}
}
go kickBall("张三")
go kickBall("李四")
go kickBall("王二麻子")
go kickBall("刘大")
ball <- "裁判" // 开球
var c chan bool // 一个零值nil通道
<-c // 永久阻塞在此
}
func TestFeature1(t *testing.T) {
type Person struct {
Sex string
Name string
}
type Student struct {
StudentID int
Person
}
s1 := Student{
StudentID: 1,
Sex: "male",
Name: "张三",
}
t.Log(s1)
}
func TestFeature2(t *testing.T) {
// Go 1.27 之前:必须手动写出类型实参 [int],否则编译报错
fn1 := IntToString(FormatToString[int])
fn2 := IntToString(FormatToString)
oldFormat := []IntToString{FormatToString[int]}
newFormat := []IntToString{FormatToString}
t.Log(fn1(1))
t.Log(fn2(2))
for i, fn := range oldFormat {
t.Log(fn(i))
}
for i, fn := range newFormat {
t.Log(fn(i))
}
}
// 一个泛型格式化函数
func FormatToString[T any](v T) string {
return fmt.Sprintf("value: %v", v)
}
// 具名函数类型:接收一个 int,返回 string
type IntToString func(int) string
func TestSort(t *testing.T) {
slice := []int{8, 2, 4, 4}
Sort1(slice)
t.Log(slice)
}
func Sort1(slice []int) {
for i := 0; i < len(slice)-1; i++ {
for j := 0; j < len(slice)-1-i; j++ {
if slice[j] > slice[j+1] {
slice[j], slice[j+1] = slice[j+1], slice[j]
}
}
}
}
@@ -0,0 +1,34 @@
package regularmatch
import (
"regexp"
"testing"
)
func TestChineseRegExp(t *testing.T) {
reg, err := regexp.Compile("[\u4e00-\u9fa5]")
if err != nil {
t.Errorf("Failed to compile regex: %v", err)
}
matchStrSlice := reg.FindAllString("hello 世界", -1)
if len(matchStrSlice) != 2 {
t.Errorf("Expected 2 matches, got %d", len(matchStrSlice))
}
if matchStrSlice[0] != "世" || matchStrSlice[1] != "界" {
t.Errorf("Expected [世, 界], got %v", matchStrSlice)
}
}
func TestChinaMobile(t *testing.T) {
reg, err := regexp.Compile(`^1(3\d|4[5-9]|5[0-35-9]|6[2567]|7[0-8]|8\d|9[0-35-9])\d{8}$`)
if err != nil {
t.Errorf("Failed to compile regex: %v", err)
}
// matchStrSlice := reg.FindAllString("123-12345678 1234-12345678", -1)
// if len(matchStrSlice) != 2 {
// t.Errorf("Expected 2 matches, got %d", len(matchStrSlice))
// }
matchStrSlice := reg.FindAllString("17623862704", -1)
t.Log(matchStrSlice[0])
}
+18
View File
@@ -0,0 +1,18 @@
package main
import (
"fmt"
"testing"
)
func appendSlice(s []int, n int) {
s = append(s, n)
fmt.Printf("len=%d,cap=%d\n", len(s), cap(s))
}
func TestSliceAppend(t *testing.T) {
s1 := make([]int, 5)
appendSlice(s1, 10)
if len(s1) != 5 || cap(s1) != 5 {
t.Errorf("Expected len=5, cap=5; got len=%d, cap=%d", len(s1), cap(s1))
}
}