这篇文章基于我今天写的代码整理,包含 hello world、输入输出、变量、字符串、条件、循环、数组、切片,以及我今天踩过的坑


1. Go 程序的最小结构

package main

import "fmt"

func main() {
    fmt.Println("hello world")
}

核心就三个东西:

  • package main:声明当前文件属于 mainmain 包可以编译成可执行程序
  • import "fmt":导入格式化输出包,类似 C++ 的 #include <iostream>,但 Go 的 import 是包导入,不是文本展开
  • func main():程序入口

注意:package main 必须是文件第一条非注释语句,import 必须在函数声明前面

我今天犯过的错是把 package mainimport "fmt" 写到了函数后面,这是不合法的

错误:

func str() {
}

package main
import "fmt"

正确:

package main

import "fmt"

func str() {
}

本质:Go 编译器必须先知道这个文件属于哪个包,才能继续解析后面的代码


2. 输出:Println、Printf、Sprintf

Println

fmt.Println("hello")
fmt.Println("go", "backend", 8080)

Println 会自动换行,多个参数之间自动加空格

Printf

model := "Openai"
qps := 200
enabled := true
fmt.Printf("model: %s\nqps: %d\nenabled: %t\n", model, qps, enabled)

常用格式:

格式 含义
%s 字符串
%d 整数
%t bool
%f 浮点数
%v 通用格式
%T 打印类型
%q 带引号字符串
%c 字符

Sprintf

msg := fmt.Sprintf("service: %s, port: %d", "ai-backend", 8090)
fmt.Println(msg)

Sprintf 不直接输出,而是生成字符串


3. 读取配置:环境变量更像后端写法

命令行输入可以用 fmt.Scanln,但后端服务更常见的是读环境变量:

package main

import (
    "fmt"
    "os"
)

func main() {
    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }
    fmt.Println("port =", port)
}

这个逻辑很常用:优先读环境变量,没有就给默认值


4. 变量声明:Go 不喜欢隐式和模糊

几种写法:

var age int = 20
var name = "Vect"
hobby := "hiking"

说明:

  • var age int = 20:显式声明类型
  • var name = "Vect":自动类型推导
  • hobby := "hiking":短变量声明,只能在函数内部使用

一次声明多个变量:

var a1, a2, a3 int = 1, 2, 3

零值

Go 变量只声明不赋值,也有默认值:

var cnt int16
var name string
var ok bool

fmt.Printf("count=%d\n", cnt)
fmt.Printf("name=%q\n", name)
fmt.Printf("ok=%v\n", ok)

输出:

count=0
name=""
ok=false

C++ 局部变量不初始化可能是随机值,Go 不会 Go 会给零值

常见零值:

类型 零值
int / float 0
string ""
bool false
pointer / slice / map nil

5. 类型转换:Go 不自动帮你转

var a int16 = 10
var b int32 = 20
fmt.Println(int32(a) + b)

必须写 int32(a)

下面这样不行:

fmt.Println(a + b)

因为 aint16bint32,类型不同

C++ 里经常自动类型提升,Go 不这么干 Go 的态度是:不同类型就是不同类型,想算就明确转换

Go 讨厌隐藏行为,强调代码显式、清楚


6. 字符串:byte、rune、len

这是今天最容易踩坑的地方

len 返回字节数,不是字符数

s := "Go语言"
fmt.Println(len(s))

结果是 8,不是 4

原因:

  • G:1 字节
  • o:1 字节
  • :UTF-8 下 3 字节
  • :UTF-8 下 3 字节

所以总共 1 + 1 + 3 + 3 = 8

s[0] 取的是 byte

s := "golong"
fmt.Println("first byte: ", s[0])

输出:

first byte:  103

为什么不是 g

因为 s[0] 取的是第 0 个字节,类型是 byte,本质是 uint8 字符 g 的 ASCII / UTF-8 编码值就是 103

如果想按字符输出:

fmt.Printf("first byte: %c\n", s[0])

range 字符串拿到的是 rune

for index, r := range s {
    fmt.Printf("index=%d rune=%c\n", index, r)
}
  • index 是字节下标
  • r 是 rune,也就是 Unicode 码点

总结:

写法 含义
len(s) 字节数
s[i] 第 i 个字节,byte
range s 按 rune 遍历字符串

处理中文时,不能随便用 s[i] 当字符


7. if:条件不用括号,可以带短语句

基础写法:

num := 100
if num <= 80 {
    fmt.Println("low")
} else {
    fmt.Println("high")
}

Go 的 if 条件不需要小括号

还可以在 if 前写短语句:

if score := 92; score >= 90 {
    fmt.Println("优秀")
} else {
    fmt.Println("平常")
}

这里 score 只在这个 if-else 内部有效

这个设计很适合临时变量,避免污染外层作用域


8. switch:默认 break,比 C++ 安全

status := "running"

switch status {
case "queued":
    fmt.Println("waiting")
case "running":
    fmt.Println("processing")
case "done":
    fmt.Println("finished")
default:
    fmt.Println("unknown")
}

Go 的 switch-case 默认不会向下穿透,不需要手写 break

C++ 如果忘了 break,会继续执行后面的 case;Go 默认不会

多个 case 可以合并:

switch status {
case "created", "queued":
    fmt.Println("not started")
case "running":
    fmt.Println("running")
case "done", "failed":
    fmt.Println("finished")
}

9. for:Go 只有 for

Go 没有 while,也没有 do while,只有 for

基础 for:

for i := 0; i < 3; i++ {
    fmt.Printf("%d ", i)
}

while 写法:

retry := 0
for retry < 3 {
    fmt.Println("retry", retry)
    retry++
}

死循环:

for {
    fmt.Println("running")
}

range 遍历:

models := []string{
    "qwen",
    "openai",
    "claude",
    "deepseek",
}

for index, model := range models {
    fmt.Println(index, model)
}

如果不要下标,用 _ 忽略:

for _, model := range models {
    fmt.Println(model)
}

10. 多行切片最后为什么要逗号?

正确写法:

models := []string{
    "qwen",
    "openai",
    "claude",
    "deepseek",
}

最后一个元素后面也要有逗号

如果写成多行,最后一个元素后面必须保留逗号,原因是 Go 会在行尾自动插入分号,如果没有逗号,编译器会把最后一个元素那一行当成语句结束,导致复合字面量解析失败

单行可以不写最后逗号:

models := []string{"qwen", "openai", "claude", "deepseek"}

多行保留最后逗号还有一个好处:以后追加元素时,只新增一行,不用修改上一行


11. 数组:定长,长度是类型的一部分

var nums [3]int = [3]int{8080, 8081, 8082}
fmt.Println(nums)

也可以写:

nums := [3]int{8080, 8081, 8082}

Go 数组是定长的,[3]int[4]int 是两种不同类型

这一点和 C++ 数组有点像,长度固定 但 Go 实际开发里更常用的是切片


12. 切片:不是数组,但可以先当“变长数组”理解

models := []string{"qwen", "openai"}
models = append(models, "llama")

fmt.Println(models)
fmt.Println("len ", len(models))

[]string 里面没有长度,所以这是切片

数组:

[3]string

切片:

[]string

初学时可以把切片理解成“变长数组”,但本质上切片不是数组本身

切片底层大概由三部分组成:

指向底层数组的指针 + 长度 len + 容量 cap

所以切片更像是对底层数组某一段的描述

append 要接收返回值:

models = append(models, "llama")

不要只写:

append(models, "llama")

因为 append 可能会返回新的切片,不接住就丢了


13. nil 切片和 make

var s []string
fmt.Println("uninit:", s, s == nil, len(s) == 0)

这里 s 是 nil 切片

它满足:

s == nil
len(s) == 0

然后用 make 创建切片:

s = make([]string, 3)
fmt.Println("emp:", s, "len: ", len(s), "cap:", cap(s))

make([]string, 3) 表示创建长度为 3 的字符串切片 里面每个元素是 string 的零值,也就是空字符串

lencap

名称 含义
len 当前切片长度,可以访问的元素个数
cap 从切片起点到底层数组末尾的容量

14. 二维切片:[][]int

func slice3() {
    t := make([][]int, 3)

    for i := 0; i < 3; i++ {
        rowLen := i + 1
        t[i] = make([]int, rowLen)

        for j := 0; j < rowLen; j++ {
            t[i][j] = i + j
        }
    }

    fmt.Println("二维切片: ", t)
}

输出大概是:

二维切片:  [[0] [1 2] [2 3 4]]

这里严格来说不是二维数组,而是二维切片

数组写法:

[3][4]int

切片写法:

[][]int

二维切片的每一行长度可以不同,所以它可以表示不规则矩阵

另外,不建议把变量命名为 len

len := i + 1

虽然能写,但会遮蔽 Go 内置函数 len 更好的名字是:

rowLen := i + 1

15. copy 字符串:目标应该是 []byte

我一开始写过类似这样的代码:

s := "hello, golong"
str := make([]string, len(s))
copy(str, s)

这是错的

原因:

  • sstring
  • str[]string
  • copy 不支持 copy([]string, string)

字符串本质是字节序列,所以如果要复制字符串内容,目标应该是 []byte

s := "hello, golong"
str := make([]byte, len(s))
copy(str, s)
fmt.Println("copy: ", str)

输出的是字节值:

copy:  [104 101 108 108 111 44 32 103 111 108 111 110 103]

如果想重新看成字符串:

fmt.Println(string(str))

16. 字符串切片表达式:左闭右开

s := "hello, golong"

l := s[2:5]
fmt.Println("sl1: ", l)

l = s[:5]
fmt.Println("sl2: ", l)

l = s[2:]
fmt.Println("sl3: ", l)

切片规则是左闭右开:

s[low:high]

包含 low,不包含 high

字符串下标:

h e l l o ,   g o l o n g
0 1 2 3 4 5 6 7 8 9 10 11 12

所以:

s[2:5]

取到的是下标 2、3、4,也就是:

llo

但是要注意,字符串切片是按字节切,不是按字符切 英文没问题,中文要小心

如果要安全处理中文,建议转成 []rune

s := "你好"
r := []rune(s)
fmt.Println(string(r[:1]))

17. 今天犯的错误汇总

1. package mainimport 写错位置

本质:Go 文件必须先声明包,再导入包,再写函数

2. s[0] 输出 103

本质:s[0] 是 byte,不是字符 g 的编码值是 103 想输出字符用 %c

3. 多行切片最后一个元素漏逗号

本质:Go 会自动插入分号,多行复合字面量最后要保留逗号

4. fmt.Println("二维数组: " t) 少逗号

本质:函数多个参数之间必须用逗号隔开

正确:

fmt.Println("二维切片: ", t)

5. copy([]string, string) 类型错

本质:字符串是字节序列,要复制到 []byte,不是 []string

6. 把二维切片叫成二维数组

本质:[][]int 是二维切片,[3][4]int 才是二维数组

7. 用 len 当变量名

本质:会遮蔽内置函数 len,不推荐


18. C++ 转 Go 第一天的核心感受

Go像是,那晚C和Python都喝多了的样子

Go 很克制:

  • 没有隐式类型转换
  • 只有 for
  • switch 默认不穿透
  • 未使用变量会报错
  • 包结构必须清晰
  • 格式基本交给 gofmt
  • 数组和切片分得很清楚

Go 的目标不是让语法多炫,而是让工程代码统一、清楚、好维护

感觉学了C++,后面啥语言都清爽好多,世人苦C++久矣 😭

第一天最该记住的本质:

  1. Go 文件从 package 开始
  2. 可执行程序是 package main + func main()
  3. := 很常用,但只能在函数内
  4. Go 有零值,不会出现 C++ 未初始化局部变量那种随机值
  5. Go 不喜欢隐式转换,不同类型要显式转
  6. 字符串是字节序列,len 返回字节数
  7. s[i] 是 byte,range s 是 rune
  8. Go 只有 for
  9. 数组 [N]T 定长,切片 []T 动态
  10. 切片本质是底层数组的视图,包含指针、长度、容量
  11. 多行字面量最后保留逗号
  12. 中文字符串处理要注意 UTF-8,必要时用 []rune
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐