1变量的声明
1.1go的变量声明是放在变量名后面
var 是 Go 里声明变量的关键字,var 就是"我要声明一个变量"的标记。
Go
1.完整声明
var port int
var host string
var timeout time.Duration //time包里的类型,单位为ns
2.有初始值,可以推断其类型
var port = 8080
var host = "127.0.0.1"
3.短声明(只能在函数内部用)
port := 8080
host := "127.0.0.1"
time := 500*time.Millisecond //ms
1.2再补充一下time包

1.3零值
Go 的变量声明后一定有零值,不存在"未初始化"的悬空状态:
| 类型 | 零值 |
|---|---|
int |
0 |
string |
"" |
bool |
false |
| 指针、切片、map、channel、接口 | nil |
C++ 里 int port; 是未定义值,Go 里 var port int 一定是 0。这意味着你不需要手动初始化 ,但也意味着忘记赋值时不会报错,而是拿到零值,逻辑错误更隐蔽。
1.4多变量声明
Go
1.声明多个变量
var a, b, c int
2.同时赋值
d, e := 1, 2
3.变量交换值(需要中间的临时值)
d, e = e, d
2.打括号需要跟在同一行
错误写法
Go
func main()
{ // 编译错误!
fmt.Println("hi")
}
编译器报错信息通常是 syntax error: unexpected semicolon or newline before {。
原因:自动分号插入
Go 编译器在每一行末尾自动插入分号 ,规则是:如果一行以标识符、数字、字符串、)、]、}、++、-- 等结尾,就在行尾加 ;。所以上面的代码被编译器看成:
Go
func main(); // 编译器自动加了分号
{ // 这里就变成孤立的代码块了
...
}
正确写法
Go
func main() {
fmt.Println("hi")
}
if port > 0 {
fmt.Println("open")
}
for i := 0; i < 10; i++ {
fmt.Println(i)
}
唯一例外:else 必须紧跟 } 同一行
Go
// 正确
if err != nil {
return
} else {
fmt.Println("ok")
}
// 错误:else 不能另起一行
if err != nil {
return
}
else { // 编译错误!
...
}
3. 没有 while,只有 for
Go 的 for 有三种形式,覆盖 C++ 的 for、while、do-while(没有 do-while,用 for 模拟)。
形式一:经典三段式(等同 C++ for)
Go
形式一:经典三段式(等同 C++ for)
go// C++: for (int i = 0; i < 10; i++)
for i := 0; i < 10; i++ {
fmt.Println(i)//打印
}
// 你的扫描器里:
for port := start; port <= end; port++ {
scanPort(target, port)
}
形式二:只有条件(等同 C++ while)
Go
// C++: while (port < 100) { port++ }
port := 1
for port < 100 {
port++
}
// 无限循环,等同 while(true)
for {
// 需要 break 退出
if done {
break
}
}
形式三:for range(遍历切片、map、channel)
Go
ports := []int{22, 80, 443, 8080} //数组
// 只要值
for _, p := range ports {
fmt.Println(p)
}
// 要索引和值
for i, p := range ports {
fmt.Printf("index=%d port=%d\n", i, p)
}
// 遍历 channel(你收扫描结果时用)//result是一个map
for openPort := range results {
fmt.Println(openPort)
}
range 对切片返回 (索引, 值),对 map 返回 (键, 值),对 channel 只返回值(没有索引,所以 for v := range ch)。_ 是空白标识符,用来丢弃不需要的返回值,因为 Go 要求所有声明的变量必须被使用,否则编译报错。
4. 数组 vs 切片
1.数组:固定长度,值语义
Go
var arr [3]int // 三个元素,都是 0
arr[0] = 1
arr[1] = 2
arr[2] = 3
fmt.Println(len(arr)) // 3,长度是类型的一部分
数组是值类型 ,赋值和传参会复制整个数组:
Go
a := [3]int{1, 2, 3}
b := a // b 是 a 的副本
b[0] = 100
fmt.Println(a[0]) // 1,a 不受影响
2.切片:可变长度,引用语义
切片是日常使用的"动态数组",底层是一个指向数组的指针 + 长度 + 容量。
Go
// 创建方式一:字面量
ports := []int{22, 80, 443}
// 创建方式二:make(类型, 长度, 容量)
s := make([]int, 0) // 长度 0,容量 0
s := make([]int, 0, 10) // 长度 0,容量 10(预留空间,避免频繁扩容)
s := make([]int, 5) // 长度 5,容量 5,五个元素都是 0
// 创建方式三:从数组切
arr := [5]int{1, 2, 3, 4, 5}
s := arr[1:4] // [2, 3, 4],左闭右开
日常最常用 :var s []int(收集结果)和 s := []int{...}(有初始值)。make([]int, 0, N) 只在明确知道大概要存多少、想避免扩容时用。
注意 make([]int, 5) 和 make([]int, 0, 5) 的区别:
Go
a := make([]int, 5) // [0 0 0 0 0],len=5,可以直接 a[0] 访问
b := make([]int, 0, 5) // [],len=0,不能 b[0](panic),只能 append
make([]int, 5) 是"已经存在 5 个零值元素",不是"预留 5 个空位"。收集结果时用 var s []int 或 make([]int, 0),不要用 make([]int, N),否则 append 后会多出一堆你没想要的 0。
2.1len 和 cap 的区别
Go
s := make([]int, 0, 5)
fmt.Println(len(s)) // 0,当前有几个元素
fmt.Println(cap(s)) // 5,底层数组能装几个
s = append(s, 22)
fmt.Println(len(s)) // 1
fmt.Println(cap(s)) // 5,没超过容量,底层数组没换
s = append(s, 80, 443, 8080, 3306, 21)
fmt.Println(len(s)) // 6
fmt.Println(cap(s)) // 10(扩容了,通常翻倍)
len 决定你能访问哪些索引 (s[0] 到 s[len-1]),cap 决定 append 时要不要分配新数组 。len <= cap 永远成立。
2.2append 的两种情形
append 是内置函数,签名大致是 func append(s []T, elems ...T) []T,必须接收返回值。
情形一:容量够(len < cap)------原地写,不换数组
Go
s := make([]int, 0, 5) // {array: A, len: 0, cap: 5}
s = append(s, 22) // 写 A[0],返回 {A, 1, 5}
s = append(s, 80) // 写 A[1],返回 {A, 2, 5}
底层数组始终是 A,append 写的永远是 A[len] 这个新位置 ,已有元素不动。但 len 变了 ,len 在切片头里,所以必须把返回的新切片头赋回去。
情形二:容量不够(len == cap)------分配新数组
Go
s := []int{22, 80, 443} // {array: A, len: 3, cap: 3}
s = append(s, 8080) // 分配新数组 B(cap 通常 6),拷贝 22 80 443,写 B[3]=8080,返回 {B, 4, 6}
地址从 A 变成 B ,旧数组 A 被丢弃(如果没有其他切片共享它)。不赋值的话,s 还指向 A,8080 在 B 里,完全看不到。
为什么必须赋值
append返回的是新的切片头 (len至少变了,扩容时array也变了)。切片头是值类型,append拿不到你变量的地址,只能返回新值让你赋回去。不赋值,底层数据可能写进去了,但len没更新,你访问不到;扩容时新数组直接丢。切片是一个结构体,无法直接修改它的原结构体,append(s,8080)是一个临时变量,需要把值赋值回去