Go语言:加密与解密详解

文章目录

一、基础概念与术语

在当今这个数据安全至关重要的时代,加密是保护敏感信息(如用户密码、个人身份信息、支付数据等)不被未授权访问的核心技术。Go 语言通过其标准库 crypto 提供了丰富且强大的加密功能。crypto 包下包含了多个子包,分别用于实现不同类型的加密算法和工具。

1.1 几个基本概念

在开始编码之前,理解几个基本概念至关重要:

  • 明文:未加密的原始数据。
  • 密文:加密后的数据。
  • 加密:将明文转换为密文的过程。
  • 解密:将密文还原为明文的过程。
  • 密钥:在加密和解密算法中使用的秘密参数。
  • 算法:执行加密和解密的数学函数。
  • 哈希 :一种单向加密算法。它将任意长度的输入(明文)转换成一个固定长度的输出(哈希值或摘要)。这个过程是不可逆的,你无法从哈希值反推出原始明文。常用于密码存储和数据完整性校验。
  • 对称加密 :加密和解密使用同一个密钥。优点是速度快,适合加密大量数据。缺点是密钥的分发和管理比较困难。
  • 非对称加密 :使用一对密钥:公钥私钥。公钥用于加密,私钥用于解密;或者私钥用于签名,公钥用于验签。优点是密钥管理安全,缺点是速度慢,不适合加密大量数据。

1.2 使用建议

  1. 永远不要自己发明加密算法 :使用经过广泛审查和测试的标准算法,如 AES, RSA, SHA-256, PBKDF2。Go 的 crypto 包已经为你做好了这些。
  2. 密钥管理是最大的挑战
    • 不要硬编码密钥:将密钥直接写在源代码中是极其危险的行为。一旦代码泄露,密钥就泄露了。
    • 使用安全的密钥存储 :对于生产环境,应使用专门的密钥管理服务,如 HashiCorp Vault , AWS KMS , Google Cloud KMSAzure Key Vault
    • 环境变量:对于简单的应用,可以通过环境变量注入密钥,这比硬编码好,但也不是最安全的方案(环境变量可能被其他进程读取)。
  3. 使用正确的工具做正确的事
    • 存储密码 :使用 带盐的强哈希 ,如 bcrypt (推荐), scryptPBKDF2绝对不要使用可逆加密(如 AES)或普通哈希(如 SHA-256)来存储密码golang.org/x/crypto/bcrypt 是处理密码的最佳选择。
    • 加密数据 :使用 AES-GCM 进行对称加密。它既快又安全,还内置了认证。
    • 安全传输 :使用 TLS/SSL。这是保护网络通信的标准,不要自己实现传输层加密。
    • 数字签名 :使用 RSA-PSSECDSA
  4. 使用安全的随机数生成器 :任何需要随机性的地方(如生成 Nonce, IV, Salt),都必须使用 crypto/rand,而不是 math/randmath/rand 是伪随机的,可预测,不适用于安全场景。
  5. 处理错误:加密操作中的错误往往是安全问题的信号。例如,解密失败很可能意味着密钥错误或数据被篡改。务必妥善处理这些错误,不要简单地忽略。
  6. 保持更新:加密领域在不断进步,旧的算法可能会被发现漏洞。关注 Go 官方博客和安全社区的建议,及时更新你的加密实践。

二、哈希(单向加密)

哈希函数不是用来加密解密的,而是用来生成数据的"指纹"。最常见的应用场景是存储用户密码和校验文件完整性。

2.1 基本哈希函数 (MD5, SHA-256)

Go 的 crypto 包提供了多种哈希算法的实现。MD5 和 SHA-1 由于存在安全漏洞,已不推荐用于安全目的,但了解它们仍有意义。SHA-256 是目前最常用的安全哈希算法之一。

案例代码:计算字符串的哈希值

go 复制代码
package main
import (
	"crypto/md5"
	"crypto/sha1"
	"crypto/sha256"
	"crypto/sha512"
	"fmt"
)
func main() {
	data := []byte("Hello, Go Encryption!")
	// MD5 (不安全,仅作演示)
	md5Hash := md5.Sum(data)
	fmt.Printf("MD5: %x\n", md5Hash)
	// SHA-1 (不安全,仅作演示)
	sha1Hash := sha1.Sum(data)
	fmt.Printf("SHA-1: %x\n", sha1Hash)
	// SHA-256 (推荐)
	sha256Hash := sha256.Sum256(data)
	fmt.Printf("SHA-256: %x\n", sha256Hash)
	// SHA-512
	sha512Hash := sha512.Sum512(data)
	fmt.Printf("SHA-512: %x\n", sha512Hash)
}

输出示例:

复制代码
MD5: 9e7b5c9e9e5c9e9e5c9e9e5c9e9e5c9e
SHA-1: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2
SHA-256: 2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae
SHA-512: 9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043

注意%x 是一个格式化动词,用于将字节切片以十六进制字符串的形式输出。

2.2 HMAC (Hash-based Message Authentication Code)

HMAC 是一种基于哈希的消息认证码。它需要一个密钥和一个消息作为输入,输出一个固定长度的哈希值。HMAC 不仅可以验证数据的完整性,还可以验证消息的来源(因为只有拥有相同密钥的人才能生成相同的 HMAC)。它比单纯的哈希更安全,可以有效防止"长度扩展攻击"。

案例代码:生成和验证 HMAC

go 复制代码
package main
import (
	"crypto/hmac"
	"crypto/sha256"
	"fmt"
)
func main() {
	message := []byte("This is a secret message.")
	// 在实际应用中,这个密钥应该安全地存储,不能硬编码在代码里
	key := []byte("my-super-secret-key-12345")
	// --- 生成 HMAC ---
	// 创建一个使用 SHA256 算法的 HMAC 实例
	h := hmac.New(sha256.New, key)
	// 写入数据
	h.Write(message)
	// 计算最终的 HMAC 值
	mac := h.Sum(nil)
	fmt.Printf("HMAC-SHA256: %x\n", mac)
	// --- 验证 HMAC ---
	// 接收方收到 message 和 mac 后,用同样的 key 重新计算一次
	receivedMessage := []byte("This is a secret message.")
	receivedMac := mac // 假设这是从网络或文件中收到的 MAC
	// 创建新的 HMAC 实例进行验证
	h2 := hmac.New(sha256.New, key)
	h2.Write(receivedMessage)
	expectedMac := h2.Sum(nil)
	// 使用 hmac.Equal 进行安全比较,防止时序攻击
	if hmac.Equal(receivedMac, expectedMac) {
		fmt.Println("HMAC verification: SUCCESS! The message is authentic and intact.")
	} else {
		fmt.Println("HMAC verification: FAILED! The message may have been tampered with.")
	}
}

三、对称加密(AES)

AES (Advanced Encryption Standard) 是目前最流行和安全的对称加密标准。Go 的 crypto/aes 包提供了 AES 的实现。

AES 有几种工作模式,如 ECB, CBC, GCM 等。

  • ECB (Electronic Codebook) :不安全,因为相同的明文块会生成相同的密文块。切勿使用
  • CBC (Cipher Block Chaining) :每个明文块在加密前会与前一个密文块进行异或操作。需要一个初始化向量 来加密第一个块。IV 不需要保密,但必须是不可预测的,通常每次加密都随机生成。
  • GCM (Galois/Counter Mode) :是目前推荐的模式。它不仅提供了加密,还提供了认证(像 HMAC 一样),可以同时保证数据的机密性完整性 。它比 CBC + HMAC 的组合更高效、更简洁。
    我们将重点介绍 AES-GCM

3.1 AES-GCM 加密与解密

案例代码:使用 AES-GCM 进行加密和解密

go 复制代码
package main
import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"io"
)
// GCMEncrypt 使用 AES-GCM 加密明文
// 返回: 密文 (nonce + ciphertext), 错误
func GCMEncrypt(key []byte, plaintext []byte) ([]byte, error) {
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}
	// 推荐 GCM 模式
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return nil, err
	}
	// Nonce (Number used once) 必须是 GCM.NonceSize() 长度,并且每次加密都应该是唯一的
	// 这里我们使用 crypto/rand 生成一个随机的 nonce
	nonce := make([]byte, gcm.NonceSize())
	if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
		return nil, err
	}
	// Seal 会将 nonce 和密文拼接在一起返回
	// 格式: nonce + ciphertext + tag
	// tag 是用于认证的,GCM 内部会自动处理
	ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)
	return ciphertext, nil
}
// GCMDecrypt 使用 AES-GCM 解密密文
// 密文格式: nonce + ciphertext + tag
func GCMDecrypt(key []byte, ciphertext []byte) ([]byte, error) {
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return nil, err
	}
	nonceSize := gcm.NonceSize()
	if len(ciphertext) < nonceSize {
		return nil, fmt.Errorf("ciphertext too short")
	}
	// 从密文中分离出 nonce
	nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
	// Open 会自动验证 tag,如果验证失败会返回 error
	plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
	if err != nil {
		return nil, err
	}
	return plaintext, nil
}
func main() {
	// AES-256 需要 32 字节的密钥
	// 在实际应用中,这个密钥应该从安全的来源获取,绝对不能硬编码!
	key, _ := hex.DecodeString("6368616e676520746869732070617373776f726420746f206120736563726574")
	plaintext := []byte("This is a top-secret message that must be protected.")
	fmt.Printf("Original Plaintext: %s\n", plaintext)
	fmt.Printf("Key (hex): %x\n", key)
	// --- 加密 ---
	ciphertext, err := GCMEncrypt(key, plaintext)
	if err != nil {
		panic(err)
	}
	fmt.Printf("Ciphertext (hex): %x\n", ciphertext)
	// --- 解密 ---
	decryptedPlaintext, err := GCMDecrypt(key, ciphertext)
	if err != nil {
		panic(err)
	}
	fmt.Printf("Decrypted Plaintext: %s\n", decryptedPlaintext)
}

四、非对称加密(RSA)

RSA 是最著名的非对称加密算法。它主要用于两个场景:

  1. 加密:用公钥加密,私钥解密。常用于加密少量数据,比如对称加密的密钥。
  2. 数字签名:用私钥签名,公钥验签。用于验证身份和消息的不可否认性。

4.1 RSA 密钥对生成

首先,我们需要生成一对公钥和私钥。
案例代码:生成 RSA 密钥对并保存到文件

go 复制代码
package main
import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"encoding/pem"
	"fmt"
	"os"
)
// generateRSAKeys 生成 RSA 密钥对并保存到文件
func generateRSAKeys(bits int, privateKeyFile, publicKeyFile string) error {
	// 1. 生成私钥
	privateKey, err := rsa.GenerateKey(rand.Reader, bits)
	if err != nil {
		return err
	}
	// 2. 将私钥序列化为 ASN.1 DER 格式
	privateKeyDER := x509.MarshalPKCS1PrivateKey(privateKey)
	// 3. 创建 PEM 块
	privateKeyPEM := &pem.Block{
		Type:  "RSA PRIVATE KEY",
		Bytes: privateKeyDER,
	}
	// 4. 将私钥 PEM 块写入文件
	privateFile, err := os.Create(privateKeyFile)
	if err != nil {
		return err
	}
	defer privateFile.Close()
	if err := pem.Encode(privateFile, privateKeyPEM); err != nil {
		return err
	}
	fmt.Printf("Private key saved to %s\n", privateKeyFile)
	// 5. 从私钥中提取公钥
	publicKey := &privateKey.PublicKey
	// 6. 将公钥序列化为 ASN.1 DER 格式
	publicKeyDER, err := x509.MarshalPKIXPublicKey(publicKey)
	if err != nil {
		return err
	}
	// 7. 创建 PEM 块
	publicKeyPEM := &pem.Block{
		Type:  "PUBLIC KEY",
		Bytes: publicKeyDER,
	}
	// 8. 将公钥 PEM 块写入文件
	publicFile, err := os.Create(publicKeyFile)
	if err != nil {
		return err
	}
	defer publicFile.Close()
	if err := pem.Encode(publicFile, publicKeyPEM); err != nil {
		return err
	}
	fmt.Printf("Public key saved to %s\n", publicKeyFile)
	return nil
}
func main() {
	// 生成 2048 位的 RSA 密钥对
	err := generateRSAKeys(2048, "private.pem", "public.pem")
	if err != nil {
		fmt.Println("Error generating RSA keys:", err)
	}
}

运行此代码后,会生成 private.pempublic.pem 两个文件。

4.2 RSA 加密与解密

RSA 加密的数据长度有限制,与密钥长度有关。例如,2048位的密钥最多只能加密 245 字节的数据。因此,它通常用于加密一个对称密钥,而不是直接加密大文件。
案例代码:使用 RSA 公钥加密,私钥解密

go 复制代码
package main
import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"encoding/pem"
	"fmt"
	"os"
)
// loadPrivateKey 从 PEM 文件加载私钥
func loadPrivateKey(filename string) (*rsa.PrivateKey, error) {
	keyBytes, err := os.ReadFile(filename)
	if err != nil {
		return nil, err
	}
	block, _ := pem.Decode(keyBytes)
	if block == nil {
		return nil, fmt.Errorf("failed to decode PEM block containing private key")
	}
	privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
	if err != nil {
		return nil, err
	}
	return privateKey, nil
}
// loadPublicKey 从 PEM 文件加载公钥
func loadPublicKey(filename string) (*rsa.PublicKey, error) {
	keyBytes, err := os.ReadFile(filename)
	if err != nil {
		return nil, err
	}
	block, _ := pem.Decode(keyBytes)
	if block == nil {
		return nil, fmt.Errorf("failed to decode PEM block containing public key")
	}
	publicKey, err := x509.ParsePKIXPublicKey(block.Bytes)
	if err != nil {
		return nil, err
	}
	rsaPublicKey, ok := publicKey.(*rsa.PublicKey)
	if !ok {
		return nil, fmt.Errorf("not an RSA public key")
	}
	return rsaPublicKey, nil
}
func main() {
	// 假设我们已经通过上一个例子生成了密钥对
	privateKey, err := loadPrivateKey("private.pem")
	if err != nil {
		panic(err)
	}
	publicKey, err := loadPublicKey("public.pem")
	if err != nil {
		panic(err)
	}
	plaintext := []byte("This is a secret message for RSA encryption.")
	fmt.Printf("Original Plaintext: %s\n", plaintext)
	// --- 公钥加密 ---
	// OAEP 是比 PKCS#1 v1.5 更安全的填充方案
	ciphertext, err := rsa.EncryptOAEP(
		sha256.New(),
		rand.Reader,
		publicKey,
		plaintext,
		nil, // 可选的标签
	)
	if err != nil {
		panic(err)
	}
	fmt.Printf("Ciphertext (hex): %x\n", ciphertext)
	// --- 私钥解密 ---
	decryptedPlaintext, err := rsa.DecryptOAEP(
		sha256.New(),
		rand.Reader,
		privateKey,
		ciphertext,
		nil, // 可选的标签
	)
	if err != nil {
		panic(err)
	}
	fmt.Printf("Decrypted Plaintext: %s\n", decryptedPlaintext)
}

注意 :为了运行上面的代码,你需要先导入 crypto/sha256 包。

五、密钥派生函数

用户输入的密码通常不够强壮,不能直接用作加密密钥。密钥派生函数可以从一个低熵的密码(可能还加上一个"盐"值)中派生出一个高熵的、适合加密的密钥。

5.1 PBKDF2

PBKDF2 (Password-Based Key Derivation Function 2) 是一个广泛使用的密钥派生函数。它通过多次哈希迭代来增加暴力破解的难度。

案例代码:使用 PBKDF2 从密码派生密钥

go 复制代码
package main
import (
	"crypto/rand"
	"crypto/sha256"
	"fmt"
	"golang.org/x/crypto/pbkdf2"
)
func main() {
	password := []byte("my-weak-password-123")
	// 盐值应该是随机的,并且与密文一起存储
	salt := make([]byte, 16)
	if _, err := rand.Read(salt); err != nil {
		panic(err)
	}
	// 迭代次数,越高越安全,但也越慢
	iterations := 100000
	// 想要的密钥长度,例如 AES-256 需要 32 字节
	keyLength := 32
	// 使用 PBKDF2 派生密钥
	// 参数: 密码, 盐值, 迭代次数, 密钥长度, 哈希函数
	derivedKey := pbkdf2.Key(password, salt, iterations, keyLength, sha256.New)
	fmt.Printf("Password: %s\n", password)
	fmt.Printf("Salt (hex): %x\n", salt)
	fmt.Printf("Iterations: %d\n", iterations)
	fmt.Printf("Derived Key (hex): %x\n", derivedKey)
}

注意golang.org/x/crypto/pbkdf2 是 Go 的一个子仓库,提供了额外的加密算法。你需要先通过 go get golang.org/x/crypto/pbkdf2 来安装它。

六、实战案例:安全的配置文件读写

让我们结合 AES-GCMPBKDF2 来创建一个工具,它可以安全地加密一个配置文件,然后再解密它。

场景

  1. 用户有一个 config.json 文件,包含敏感信息。
  2. 用户输入一个密码。
  3. 程序使用密码派生出一个 AES 密钥,然后用这个密钥加密 config.json,生成 config.enc
  4. 程序也能读取 config.enc,用同样的密码解密,还原出 config.json

案例代码:secure_config.go

go 复制代码
package main
import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"encoding/json"
	"fmt"
	"golang.org/x/crypto/pbkdf2"
	"crypto/sha256"
	"io"
	"io/ioutil"
	"os"
)
// Config 定义我们的配置结构
type Config struct {
	DatabaseURL string `json:"database_url"`
	APIKey      string `json:"api_key"`
	Secret      string `json:"secret"`
}
// deriveKey 从密码和盐值派生 AES 密钥
func deriveKey(password string, salt []byte) []byte {
	return pbkdf2.Key([]byte(password), salt, 100000, 32, sha256.New)
}
// encryptFile 加密文件
func encryptFile(filename string, password string) error {
	// 1. 读取原始文件
	plaintext, err := ioutil.ReadFile(filename)
	if err != nil {
		return fmt.Errorf("failed to read file: %w", err)
	}
	// 2. 生成随机盐值
	salt := make([]byte, 16)
	if _, err := io.ReadFull(rand.Reader, salt); err != nil {
		return fmt.Errorf("failed to generate salt: %w", err)
	}
	// 3. 派生密钥
	key := deriveKey(password, salt)
	// 4. 创建 AES-GCM 加密器
	block, err := aes.NewCipher(key)
	if err != nil {
		return fmt.Errorf("failed to create cipher: %w", err)
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return fmt.Errorf("failed to create GCM: %w", err)
	}
	// 5. 生成 Nonce
	nonce := make([]byte, gcm.NonceSize())
	if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
		return fmt.Errorf("failed to generate nonce: %w", err)
	}
	// 6. 加密
	ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)
	// 7. 将盐值和密文写入新文件 (盐值需要存储以便解密)
	// 文件格式: salt + ciphertext
	outputFile := filename + ".enc"
	err = ioutil.WriteFile(outputFile, append(salt, ciphertext...), 0644)
	if err != nil {
		return fmt.Errorf("failed to write encrypted file: %w", err)
	}
	fmt.Printf("File '%s' encrypted successfully to '%s'\n", filename, outputFile)
	return nil
}
// decryptFile 解密文件
func decryptFile(encryptedFilename string, password string) error {
	// 1. 读取加密文件
	data, err := ioutil.ReadFile(encryptedFilename)
	if err != nil {
		return fmt.Errorf("failed to read encrypted file: %w", err)
	}
	// 2. 提取盐值和密文
	// 我们知道盐值是 16 字节
	if len(data) < 16 {
		return fmt.Errorf("invalid encrypted file: too short")
	}
	salt := data[:16]
	ciphertext := data[16:]
	// 3. 派生密钥 (使用和加密时相同的密码和盐值)
	key := deriveKey(password, salt)
	// 4. 创建 AES-GCM 解密器
	block, err := aes.NewCipher(key)
	if err != nil {
		return fmt.Errorf("failed to create cipher: %w", err)
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return fmt.Errorf("failed to create GCM: %w", err)
	}
	nonceSize := gcm.NonceSize()
	if len(ciphertext) < nonceSize {
		return fmt.Errorf("invalid ciphertext: too short")
	}
	// 5. 提取 Nonce 和实际密文
	nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
	// 6. 解密
	plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
	if err != nil {
		// 这个错误通常意味着密码错误或数据被篡改
		return fmt.Errorf("decryption failed (wrong password or corrupted file?): %w", err)
	}
	// 7. 写入解密后的文件
	originalFilename := encryptedFilename[:len(encryptedFilename)-4] // 移除 .enc 后缀
	err = ioutil.WriteFile(originalFilename, plaintext, 0644)
	if err != nil {
		return fmt.Errorf("failed to write decrypted file: %w", err)
	}
	fmt.Printf("File '%s' decrypted successfully to '%s'\n", encryptedFilename, originalFilename)
	return nil
}
func main() {
	// --- 准备一个测试配置文件 ---
	config := Config{
		DatabaseURL: "postgres://user:pass@host:5432/db",
		APIKey:      "sk-1234567890abcdef",
		Secret:      "this-is-a-very-important-secret",
	}
	configData, _ := json.MarshalIndent(config, "", "  ")
	_ = ioutil.WriteFile("config.json", configData, 0644)
	fmt.Println("Created a sample config.json file.")
	// --- 加密 ---
	password := "my-super-strong-password"
	err := encryptFile("config.json", password)
	if err != nil {
		panic(err)
	}
	// 为了演示,我们删除原始文件
	_ = os.Remove("config.json")
	// --- 解密 ---
	err = decryptFile("config.json.enc", password)
	if err != nil {
		panic(err)
	}
	// --- 验证解密后的文件 ---
	decryptedConfigData, _ := ioutil.ReadFile("config.json")
	fmt.Println("\nDecrypted config content:")
	fmt.Println(string(decryptedConfigData))
}

总结 :Go 语言的 crypto 包及其子仓库(golang.org/x/crypto)为开发者提供了一套强大、现代且易于使用的加密工具箱。通过本文的学习,你应该已经掌握了:

  • 如何使用哈希(SHA-256)和 HMAC 进行数据摘要和认证。
  • 如何使用 AES-GCM 进行高效且安全的对称加密和解密。
  • 如何生成和使用 RSA 密钥对进行非对称加密。
  • 如何使用 PBKDF2 从用户密码安全地派生加密密钥。
  • 如何将这些技术组合起来,解决实际问题,如安全地存储配置文件。
相关推荐
武子康2 小时前
大数据-117 - Flink JDBC Sink 详细解析:MySQL 实时写入、批处理优化与最佳实践 写出Kafka
大数据·后端·flink
灰太狼大王灬3 小时前
Go 项目从开发到部署笔记
开发语言·笔记·golang
小树懒(-_-)3 小时前
SEO:Java项
java·开发语言
用户21411832636023 小时前
Qwen3-VL 接口部署全攻略:从源码到 Docker,手把手教你玩转多模态调用
后端
databook3 小时前
Manim实现旋转扭曲特效
后端·python·动效
karry_k3 小时前
ThreadLocal原理以及内存泄漏
java·后端·面试
胖咕噜的稞达鸭3 小时前
二叉树进阶面试题:最小栈 栈的压入·弹出序列 二叉树层序遍历
开发语言·c++
MrSun的博客3 小时前
数据源切换之道
后端
Keepreal4963 小时前
1小时快速上手SpringBoot,熟练掌握CRUD
spring boot·后端