C++转go的之路:变量声明、iota、函数、切片、init、defer

这篇文章不是罗列语法差异,而是从我 C++ 习惯开始转变,写 Go 第一天的代码出发,把每个特性背后的为什么底层是怎么做的讲清楚。读完你会理解 Go 的设计哲学,而不是记住一堆语法规则。


目录

  1. 第一印象:包、导入、分号去哪儿了
  2. 变量声明:四种方式,一种哲学
  3. [const 和 iota:C++ 枚举的优雅替代](#const 和 iota:C++ 枚举的优雅替代)
  4. 函数:多返回值改变了什么
  5. [数组 vs 切片:Go 最需要深刻理解的概念](#数组 vs 切片:Go 最需要深刻理解的概念)
  6. defer:不只是"延迟执行"
  7. [import 和 init:导包机制全解](#import 和 init:导包机制全解)
  8. [总结:Go 设计哲学一览](#总结:Go 设计哲学一览)

1. 第一印象:包、导入、分号去哪儿了

go 复制代码
package main

import (
    "fmt"
    "time"
)

func main() {
    fmt.Println("hello go!")
    time.Sleep(1 * time.Second)
}

分号去哪儿了?

Go 的编译器会在词法分析阶段自动插入分号。规则很简单:如果一行以以下 token 结尾,就自动补分号:

  • 标识符(变量名、函数名等)
  • 数字、字符串等字面量
  • breakcontinuefallthroughreturn
  • ++--)}]

这意味着你完全不用打分号。但它有个副作用:{ 必须和函数声明在同一行

go 复制代码
// 正确
func main() {

// 错误!编译器会在 func main() 后面自动加分号
func main()
{

这和 C++ 习惯不同,但写两天就适应了。实际上这个规则让 Go 的代码风格变得高度统一------所有人的代码格式化后长得一样。

package:不是 namespace

Go 的 package 和 C++ 的 namespace 完全不是一回事。package 是编译和分发的基本单元。每个 .go 文件的第一行必须是 package <名字>

  • package main 是特殊的------它告诉 Go 编译器这个包要编译成可执行文件 ,而且里面必须有 func main()

  • 其他 package 编译成库,可以被别的程序导入

    C++ namespace: 逻辑分组,可以嵌套,不影响编译
    Go package: 物理分组(目录即包),决定编译产物,不能嵌套

导包:用括号包裹多个

go 复制代码
import "fmt"           // 单个
import (               // 多个
    "fmt"
    "time"
)

需要注意的是,Go 的导入路径就是相对于 $GOROOT/src$GOPATH/src 的路径 (或者 module 路径)。导入后,使用方式是 包名.函数名,包名默认是路径的最后一段。


2. 变量声明:四种方式,一种哲学

go 复制代码
// 方式1:只声明,用默认零值
var a int           // a == 0

// 方式2:声明并初始化,显式指定类型
var b int = 100     // b == 100

// 方式3:声明并初始化,类型自动推导(类似 C++ auto)
var c = 100         // c 是 int
var cc = "aaaaa"    // cc 是 string

// 方式4:短声明,只能在函数内使用
e := 100            // 相当于 var e = 100

C++ 对比

特性 C++ Go
默认初始化 栈上的基本类型是垃圾值 永远是零值(int=0, string="", bool=false, 指针=nil)
类型推导 auto x = 1; x := 1var x = 1
类型位置 类型在变量前面 int a 类型在变量后面 a int

为什么 Go 把类型放在后面?

C++ 的 int a, b, c 很直观。但看这个:

cpp 复制代码
// C++:复杂声明时类型前置变得难以阅读
int (*fp)(int, int);  // fp 是什么?一个函数指针
// Go:类型后置,从左到右自然阅读
var fp func(int, int) int  // fp 是一个接收两个 int、返回 int 的函数

本质上,Go 的类型声明顺序就是**"变量名 类型描述"**,从左往右读,越读越具体。这和 C 的传统相反,但对于复杂类型声明更清晰。

:= 的陷阱

:= 不是赋值运算符,是声明+赋值 。它只能在函数内使用,而且左边至少要有一个新变量

go 复制代码
// 正确
a := 1
b, c := 2, 3   // b 和 c 都是新变量

// 错误
var x int
x := 1  // 编译错误:x 已经声明过了

多变量声明

go 复制代码
// 同类型连续声明
var xx, yy int = 100, 200

// 不同类型混合,各自推导
var kk, ll = 100, "hhhh"

// 括号分组(全局或局部都可以)
var (
    vv int  = 100
    jj bool = false
)

零值设计哲学

Go 没有未初始化的变量。这个设计决定影响深远------你永远不需要写 "检查变量是否初始化" 的代码。C++ 里栈上的垃圾值导致的 bug 在 Go 里根本不存在。这不是语法糖,是安全性保证。


3. const 和 iota:C++ 枚举的优雅替代

go 复制代码
const (
    A = iota   // 0
    B          // 1
    C          // 2
)

const (
    BEIJING  = iota * 10  // 0
    SHANGHAI               // 10
    GUANGZHOU              // 20
)

const (
    a, b = iota + 1, iota + 2  // iota=0: a=1, b=2
    c, d                        // iota=1: c=2, d=3
    e, f                        // iota=2: d=3, e=4

    g, h = iota * 2, iota * 3   // iota=3: g=6, h=9
    i, k                        // iota=4: i=8, k=12
)

iota 是什么?

iota 就是 C++ 里手动写的那个递增计数器,但 Go 把它内置了:

cpp 复制代码
// C++ 方式
enum {
    A = 0,
    B = 1,
    C = 2,
};
// Go 方式
const (
    A = iota  // 0
    B         // 1
    C         // 2
)

iota 规则

  1. iota 只能在 const() 块中使用
  2. 每个 const() 块中,iota 从 0 开始
  3. 每新增一行常量声明,iota 自动 +1
  4. 如果一行没有写表达式,自动继承上一行的表达式(但 iota 已经累加了)

第三和第四条凑在一起,就产生了强大的效果。比如 iota*10 这一行成为"模板",下面每一行都复用这个模板,但 iota 不断增加:

go 复制代码
const (
    BEIJING  = iota * 10  // iota = 0, BEIJING = 0
    SHANGHAI               // iota = 1, SHANGHAI = 1*10 = 10
    GUANGZHOU              // iota = 2, GUANGZHOU = 2*10 = 20
)

每行一个 iota * 10,但你不必重复写------Go 帮你继承了表达式。

一行多个常量

go 复制代码
const (
    a, b = iota + 1, iota + 2  // iota=0: a=1, b=2
    c, d                        // iota=1: c=2, d=3
)

同一行内 iota 不变(都是 0),下一行变成 1。一行多列每个列可以独立用 iota。


4. 函数:多返回值改变了什么

go 复制代码
// 基础函数定义
func foo1(a string, b int) int {
    c := 100
    return c
}

// 多返回值:匿名
func foo2(a string, b int) (int, int) {
    return 666, 777
}

// 多返回值:有名
func foo3(a string, b int) (r3 int, r4 int) {
    // r3, r4 默认值为 0
    fmt.Printf("r3 = %d, r4 = %d\n", r3, r4)
    r3 = 1
    r4 = 2
    return  // 裸 return,自动返回 r3, r4 的当前值
}

// 同类型可以合并写法
func foo4(a string, b int) (r1, r2 int) {
    r1 = 1000
    r2 = 2000
    return
}

C++ 对比:多返回值怎么实现的?

C++ 要实现"返回多个值",你有这些选择:

cpp 复制代码
// 方案1:用引用参数(调用方必须先声明变量)
void foo(int a, int& out1, int& out2);

// 方案2:返回 pair/tuple(semantics 不清晰)
std::pair<int, int> foo(int a);
// 调用方:auto [x, y] = foo(1);  // C++17 structured binding

// 方案3:定义结构体(为每个函数返回值建结构体,overkill)
struct FooResult { int x; int y; };
FooResult foo(int a);

Go 从语言层面解决了这个问题。底层实现上,Go 的返回值本质上是函数栈帧上的额外空间------调用方在 call 之前就在自己的栈上预留了返回值的位置,被调用方直接往里写。返回多个值时,就是在栈上预留了多块空间。和 C++ 的 RVO/NRVO 异曲同工,但是透明的。

命名返回值的裸 return

go 复制代码
func foo() (result int) {
    result = 10
    return  // 返回 10,等同于 return result
}

return 会返回命名返回值的当前值。这在短函数里很方便,长函数里建议还是显式写出 return 什么,否则读代码要往上翻看返回值叫什么。

参数传递:一切皆值传递(重要!)

这是 C++ 最容易搞错的地方------Go 的所有函数参数都是值传递。没有引用传递。那为什么传入 slice/map 后修改它,外部能看到?

因为 slice、map、channel 这些类型的"值"本身就是一个指向底层数据的指针结构(后面 slice 章节会详细展开)。传的是这个结构的副本,但副本里的指针还是指向同一块底层内存。

复制代码
C++:
  void f(T& x)   // 引用传递,x 是原变量的别名
  void f(T* x)   // 指针传递,x 是地址的副本
  void f(T x)    // 值传递,x 是完整拷贝

Go:
  func f(x T)    // 永远是值传递
  // 如果 T 是 slice:x 是 slice header 的副本,但 header 里的指针指向同一个底层数组
  // 如果 T 是 int:x 是整数的副本

5. 数组 vs 切片:Go 最需要深刻理解的概念

这是 C++ 转 Go 最容易被坑的地方。Go 有两种"像数组的东西",但本质完全不同。

5.1 定长数组:[N]T

go 复制代码
var myArr1 [10]int               // 10 个 0
myArr2 := [8]int{1, 2, 3, 4}    // 前 4 个指定,后面补 0

关键特性:[10]int[8]int 是两种不同的类型。数组长度是类型的一部分。

go 复制代码
fmt.Printf("myArr1 types = %T\n", myArr1)  // [10]int
fmt.Printf("myArr2 types = %T\n", myArr2)  // [8]int

数组传参是完整拷贝

go 复制代码
func printArr(myArr [8]int) {
    myArr[0] = 999  // 只修改副本,不影响外部
}

func main() {
    myArr2 := [8]int{1, 2, 3, 4}
    printArr(myArr2)
    fmt.Println(myArr2[0])  // 还是 1,没被修改
}

这和 C++ 的 void f(std::array<int, 8> arr) 一样(不是引用时)。频繁传大数组时性能很差,所以 Go 里实际使用中很少直接传数组,几乎都用切片。

5.2 for range 遍历

go 复制代码
for index, value := range myArr {
    // index 是下标,value 是元素副本
}
// 不需要 index 时用匿名变量
for _, value := range myArr {
}

_ 是 Go 的空白标识符,相当于 /dev/null------写给它的数据直接丢弃。Go 不允许声明了变量却不使用,_ 是官方提供的垃圾桶。

5.3 切片(Slice):Go 的"动态数组"

切片才是 Go 中最常用的序列容器。它的角色对应 C++ 的 std::vector + std::span

先看用法:

go 复制代码
// 方式1:字面量直接创建
myArr := []int{1, 23, 5, 6}

// 方式2:make 创建
slice2 := make([]int, 3)      // len=3, cap=3, [0,0,0]
slice3 := make([]int, 3, 5)   // len=3, cap=5, [0,0,0]

// 方式3:声明(零值是 nil)
var slice4 []int              // nil slice

5.4 切片的底层结构(核心!)

Go 的切片不是数组,而是一个描述符(header),共 24 字节:

复制代码
struct slice {
    Data *Element  // 指向底层数组的指针     (8 bytes)
    Len  int       // 当前元素个数           (8 bytes)
    Cap  int       // 底层数组从 Data 开始到末尾的长度 (8 bytes)
}
底层数组(假设容量 5):
┌─────┬─────┬─────┬─────┬─────┐
│  0  │  0  │  0  │     │     │
└─────┴─────┴─────┴─────┴─────┘
  ↑                   ↑
  Data               Data+Cap
  
  |←─── Len=3 ──→|
  |←────────── Cap=5 ──────────→|

切片变量的值就是这个 24 字节的 header。把切片传给函数时,拷贝的是 header,但 header 里的 Data 指针指向同一个底层数组。这就是为什么传切片可以在函数内修改元素------不是引用传递,而是 header 拷贝但指针相同。

Len vs Cap

  • Len :切片里现在有多少个元素。s[i] 的 i 必须 < len
  • Cap :底层数组还有多大空间。append 时如果 len < cap,直接往后写;len == cap 时触发扩容。
go 复制代码
numbers := make([]int, 3, 5)
// len=3, cap=5, [0, 0, 0]
// 可以 append 2 个元素不用扩容

5.5 append 和扩容机制

go 复制代码
numbers := make([]int, 3, 5)  // [0,0,0], len=3, cap=5

numbers = append(numbers, 999)   // [0,0,0,999],      len=4, cap=5
numbers = append(numbers, 9999)  // [0,0,0,999,9999], len=5, cap=5
numbers = append(numbers, 99999) // [0,0,0,999,9999,99999], len=6, cap=10

扩容规则(Go 1.18+ 的实现简化):

复制代码
如果原容量 < 256:新容量 = 原容量 × 2
如果原容量 ≥ 256:新容量 ≈ 原容量 × 1.25(渐进式)

扩容时会发生:

  1. 分配新的底层数组
  2. 把旧数据拷贝过去
  3. 返回一个新的 slice header(Data 指向新数组)

这就是为什么 append 的返回值必须赋值回去:

go 复制代码
numbers = append(numbers, 1)  // 必须接收返回值!

不接收的话,如果发生了扩容,numbers 还在指向旧数组,而 append 往新数组写的数据你就看不到了。

5.6 切片截取

go 复制代码
s := []int{1, 2, 3, 4, 5}  // len=5, cap=5

s1 := s[2:5]   // [3,4,5], len=3, cap=3
s2 := s[:5]    // [1,2,3,4,5], len=5, cap=5
s3 := s[5:]    // [], len=0, cap=0
s4 := s[:]     // 完整拷贝 header,共享底层数组

三索引截取:控制容量

go 复制代码
s5 := s[2:5:5]  // [begin:end:cap_end]
// s5 的 Data 指向 s[2]
// s5 的 Len = 5-2 = 3
// s5 的 Cap = 5-2 = 3(容量被限制了)

普通的 s[2:5] 的 cap 一直延伸到原数组末尾,而 s[2:5:5] 的 cap 被限制为恰好 end-begin。为什么要限制?防止后续 append 误修改原切片后面的数据。

5.7 底层数组共享的陷阱

go 复制代码
s := []int{1, 2, 3, 4, 5}
s1 := s[2:5]  // [3,4,5],但共享同一个底层数组
s1[0] = 999
fmt.Println(s)  // [1,2,999,4,5] ← s 也被改了!

图解:

复制代码
s:  Data → [1, 2, 3, 4, 5]
             ↑     ↑     ↑
             s[0]  s[2]  s[4]
                    ↑
s1: Data → ─────── [3, 4, 5]  ← s1[0] 就是 s[2]

修改 s1[0]=999 → 底层数组变成 [1,2,999,4,5]

这和 C++ 里 std::span 的行为类似,但 Go 里没有 const 保护,不小心就容易踩坑。如果不想共享底层数组,用 copy

go 复制代码
newSlice := make([]int, len(oldSlice))
copy(newSlice, oldSlice)

5.8 Go slice ≈ C++ 什么?

Go C++ 区别
[N]T std::array<T, N> Go 数组是值类型,传参是拷贝
[]T(不拥有内存) std::span<T> Go 的 slice 还带 cap,可以说都是 span
make([]T, len, cap) std::vector<T>(len) Go 的容量管理更透明
append push_back 扩容策略类似,但 Go 返回新 slice

Go 的设计非常统一:一个 []T 既是 vector 也是 span,区别只在于谁拥有底层内存。


6. defer:不只是"延迟执行"

6.1 基础:FILO 的执行顺序

go 复制代码
func testDeferOrder() {
    defer fmt.Println("defer 1 执行")
    defer fmt.Println("defer 2 执行")
    defer fmt.Println("defer 3 执行")
}
// 输出:
// defer 3 执行
// defer 2 执行
// defer 1 执行

defer 是**后进先出(FILO,栈)**的。每个 goroutine 有一个 defer 链表,执行到 defer 语句时,把它压入链表;函数返回前,从链表头部逐个取出执行。这就是为什么最后一个 defer 第一个执行。

如果熟悉 C++ 的 RAII(构造/析构配对),defer 就是更灵活的手动析构:

cpp 复制代码
// C++ RAII:获取资源时构造,离开作用域自动析构
{
    std::lock_guard<std::mutex> lock(mu);
    // ... critical section ...
}  // lock 自动释放
// Go defer:获取和释放写在一起,逻辑更直观
mu.Lock()
defer mu.Unlock()
// ... critical section ...

6.2 参数快照(最容易中招的地方)

go 复制代码
i := 0
defer fmt.Printf("defer 执行,i = %d\n", i)  // 这里 i 已经是 0 了
i = 100
// 输出:defer 执行,i = 0

defer 语句的参数在 defer 注册时就计算完毕。不是延迟求值,而是立即求值后保存。

如果把变量放在闭包里而不是参数里,效果就变了:

go 复制代码
i := 0
defer func() {
    fmt.Printf("defer 执行,i = %d\n", i)  // 闭包捕获的是 i 的引用
}()
i = 100
// 输出:defer 执行,i = 100

6.3 defer 和 return 的执行顺序(核心!)

Go 的 return 不是一个原子操作。它分为三步:

复制代码
return xxx 等价于:
    1. 返回值 = xxx        (把 xxx 赋值给返回值变量)
    2. 执行所有 defer 语句  (FILO 顺序)
    3. 真正返回            (ret 指令)

关键问题:"返回值变量"在哪里? 这取决于你有没有命名返回值。

情况1:无名返回值
go 复制代码
func testReturnAnonymous() int {
    i := 0
    defer func() { i++ }()
    return i    // 返回 0
}

执行流程:

复制代码
1. 返回值 = i          // 返回值(匿名)= 0
2. i++                 // defer 修改的是 i,不是返回值
3. 真正返回            // 返回 0

图解:

复制代码
栈帧上:
    i:    [0] → [1](defer 修改了)
    返回值: [0]       (defer 没碰到,保持不变)

因为返回值是匿名的 ,defer 里的 i++ 操作的是局部变量 i,和返回值是两个不同的内存位置。defer 无法影响返回值。

情况2:有名返回值
go 复制代码
func testReturnNamed() (i int) {
    defer func() { i++ }()
    return 0    // 返回 1!
}

执行流程:

复制代码
1. i = 0              // return 0 先把返回值变量 i 设为 0
2. i++                // defer 修改了 i,i 变成了 1
3. 真正返回           // 返回 i,即 1

图解:

复制代码
栈帧上:
    返回值 i: [0] → [1](defer 修改了这里)

有名返回值和局部变量是同一个存储位置。defer 通过闭包捕获了这个位置,所以能修改返回值。

情况3:完整流程演示
go 复制代码
func testFullProcess() (result int) {
    result = 10                  // 1. result = 10
    defer func() { result += 5 }()  // 4. result += 5 → result = 15
    return result                // 2. 返回值 = result(10)
}                                 // 3. 执行 defer
// 最终返回 15

这就是为什么很多 Go 代码用 defer 来修改返回值------在 return 之后、真正离开函数之前做一些收尾或修正。

6.4 defer 的性能开销

defer 不是免费的。每个 defer 调用在 Go 1.13 之前开销约 35ns,1.13 后优化到约 6ns。零分配的 defer(参数在寄存器里)在 Go 1.14+ 几乎和直接调用一样快。所以现在的 Go 版本里可以放心地在热路径上使用 defer。


7. import 和 init:导包机制全解

7.1 三种导入方式

go 复制代码
import (
    _ "learn/init/lib1"              // 匿名导入:只执行 init(),不使用包的 API
    mylib2 "learn/init/lib2"         // 别名导入:用 mylib2.xxx 调用
    . "fmt"                          // 点导入:直接 Println() 而不需要 fmt.Println()
)
匿名导入 _

Go 有个"讨厌"的规则:导入的包必须使用 ,不然编译错误。但有时你需要导入一个包只是为了执行它的 init()(比如数据库驱动的注册),这时就用 _

go 复制代码
import _ "learn/init/lib1"  // 只执行 lib1.init(),不调用 lib1 的方法

为什么 Go 强制"导入必须使用"?和"声明必须使用"一样------避免死代码。这看起来烦,但长期维护中非常有用。

别名导入

当两个包名冲突(比如 crypto/randmath/rand)或包名太长时:

go 复制代码
import mylib2 "learn/init/lib2"
// 然后:mylib2.Lib2Test()
点导入
go 复制代码
import . "fmt"
// 然后直接写 Println(),不需要 fmt. 前缀

这个要谨慎用,会导致命名空间污染。C++ 的 using namespace std 同理。官方建议非必要不用。

7.2 init() 函数

go 复制代码
// lib1/lib1.go
package lib1

import "fmt"

func init() {
    fmt.Println("lib1 init...")
}

func Lib1Test() {
    fmt.Println("lib1 test...")
}

init 的执行顺序

复制代码
main 包被加载
  → 它的 import 包被加载(递归)
    → lib1 被加载
      → lib1 的 import 包被加载
      → lib1 的 init() 执行
    → lib2 被加载
      → lib2 的 init() 执行
  → main 的 init() 执行(如果有)
  → main() 执行

关键规则:

  1. 同一个包的 init(),按源文件名字典序执行
  2. 不同包的 init(),按导入的依赖关系(DAG)执行,被依赖的先执行
  3. 同一个包内的多个 init(),按出现顺序执行
  4. init() 不能被手动调用

init 和 C++ 对比

cpp 复制代码
// C++:全局对象的构造函数在 main 之前执行
static DatabaseConnection db("localhost:5432");  // 在 main 之前就链接了
// Go:init() 明确地做初始化工作,语义更清晰
func init() {
    db.Connect("localhost:5432")
}

C++ 的全局/静态对象构造顺序是 implementation-defined(甚至是 undefined behavior when across translation units,这就是著名的"static initialization order fiasco")。Go 的 init 顺序是确定性的------依赖图决定顺序,不依赖则按文件名字典序。

7.3 Go 的导包和 C++ 的 include 有何本质不同?

复制代码
C++ #include:
  文本替换,把头文件的内容复制粘贴到当前文件
  问题:include 顺序影响代码、重复 include、循环 include、编译慢

Go import:
  符号级别的引用,编译时链接,不存在文本替换
  优点:import 顺序不影响结果、不存在循环 import (编译报错)、编译快

Go 编译器只靠源代码就能完成编译------没有头文件,没有前向声明。函数可以随便放在文件内的任何位置,调用的前后无关。这是 Go 编译速度极快的重要原因之一。


8. 总结:Go 设计哲学一览

从我第一天写的这些代码里,能感受到 Go 的几个核心理念:

1. 显式 > 隐式

  • 类型推导是局部的(函数内),跨文件全靠显式声明
  • 零值初始化避免了隐式的未定义行为
  • 导出 vs 非导出靠首字母大小写,一眼可见

2. 一种方式做一件事

  • 只有 for 一种循环(没有 while, do-while)
  • 只有 :=var 两种声明
  • 只有值传递一种参数传递方式

3. 不要隐藏复杂度

  • slice 的 header 设计让底层共享可见但可控
  • append 必须赋值回去,让你知道可能发生了分配
  • 数组传参就是拷贝,性能代价摆在明面上

4. 编译快是设计目标

  • 没有头文件
  • 没有泛型(Go 1.18 之前)
  • 没有继承,没有虚函数表
  • import 不是文本替换

5. 约定 > 配置

  • 目录即包名
  • 首字母大小写决定可见性
  • go fmt 统一一切

Go 不是"C++的精简版"

Go 看起来像简化版的 C,但它的设计哲学完全不一样。C++ 给你很多工具让你自由组合(模板、继承、运算符重载、RAII、智能指针...),Go 给你最少的工具但每个都足够锋利。写 Go 的感觉是"这个语言在限制我",但限制的方向是让你写出更简单、更可维护的代码。


这篇文章基于我在 写的 13 个 Go 源文件(约 450 行代码)由deepseekV4Pro模型整理而成,覆盖了从 hello world 到 slice 底层机制、defer 返回交互的全部内容。

这是代码仓库:https://github.com/WoAiXueXiHa/golong

相关推荐
fengxin_rou13 小时前
【SpringBoot+Elasticsearch 内容搜索系统实战】:架构设计与全流程实现
spring boot·后端·elasticsearch
晚烛13 小时前
CANN 自定义算子开发:Ascend C 编程接口与算子实现完整指南
c语言·开发语言·人工智能·python
问心无愧051313 小时前
ctf show web入门 254
java·开发语言·笔记
Byte Wizard13 小时前
自定义类型:结构体
c语言·开发语言
郝学胜-神的一滴13 小时前
Qt 高级开发 013: 元对象编译器(MOC)
开发语言·c++·qt·程序人生·用户界面
还是鼠鼠14 小时前
AI掘金头条新闻系统 (Toutiao News)-用户注册-生成Token
后端·python·mysql·fastapi·web
自珍JAVA20 小时前
访问者模式:让你的代码优雅地“拜访”对象结构
后端
吃好睡好便好21 小时前
用while循环语句求和
开发语言·学习·算法·matlab·信息可视化
TechWayfarer21 小时前
查询IP所在地的3种方案:从API到离线库,风控场景怎么选?
开发语言·网络·python·网络协议·tcp/ip