文章目录
- [1. 包管理](#1. 包管理)
-
- [1.1 基本概念](#1.1 基本概念)
- [1.2 包导入方法](#1.2 包导入方法)
- [1.3 可见性](#1.3 可见性)
- [1.4 Go 项目结构](#1.4 Go 项目结构)
- [2. 注释](#2. 注释)
- [3. 函数](#3. 函数)
- [4. 类型](#4. 类型)
-
- [4.1 基本类型](#4.1 基本类型)
-
- [4.1.1 数值类型(精准位数划分)](#4.1.1 数值类型(精准位数划分))
- [4.2.2 布尔类型](#4.2.2 布尔类型)
- [4.2.3 字符串类型](#4.2.3 字符串类型)
- [4.2 复合类型](#4.2 复合类型)
-
- [4.2.1 数组](#4.2.1 数组)
- [4.2.2 切片](#4.2.2 切片)
- [4.2.3 map](#4.2.3 map)
- [4.2.4 结构体](#4.2.4 结构体)
- [4.2.5 通道](#4.2.5 通道)
- [4.3 特殊类型](#4.3 特殊类型)
-
- [4.3.1 指针类型](#4.3.1 指针类型)
- [4.3.2 接口](#4.3.2 接口)
- [4.3.3 函数类型](#4.3.3 函数类型)
- [4.3.4 error 类型](#4.3.4 error 类型)
- [4.4 自定义类型](#4.4 自定义类型)
- [5. 小结](#5. 小结)
本系列文章:
1. 包管理
1.1 基本概念
Go 中 包(Package) 是代码的最小结构化单元,等价于 Java 的 Package,每个 .go 文件都属于一个包,文件的第一行就要写明 package ... 来表明当前文件属于哪个包。
包类型有两种:
- main :可以执行的程序入口,每一个应用程序都要有一个 main 包,包含
main函数,编译之后可以生成可执行文件,类比 Java 就是 main 方法。 - 普通包(工具包) :比如
fmt、net/http等,这些文件没有 main 方法,编译之后生成.a静态库,不能直接执行,类似 Java 的 util.
一般来说包名和目录名是一样的,也可以不一样,但是为了提高可读性最好一样,同一个目录下面的所有 .go 文件都 必须属于一个包。
Go 标准库就提供了一些可以直接用的包,比如经典的 fmt、net/http,就放在 $GOROOT/src 下,下面是本地的截图,可以通过 go env GOROOT 来查看 GOROOT 路径:

1.2 包导入方法
-
标准导入(最常用) :import "包路径",比如
import "fmt"。
-
别名导入 :
import f "fmt",意思是导入 fmt 用 f 来代替,避免命名冲突。
-
点导入 :
import . "fmt",直接调用函数,无需包名.
-
空白导入 :
import _ "github.com/go-sql-driver/mysql",用来初始化包,不直接使用其导出函数,用于注册驱动. -
批量导入 :用
()批量包裹导入语句(规范写法)
1.3 可见性
Go 没有 public/private/protected/default 这几个关键字,就靠首字母大小写来控制可见性:
| 标识符首字母 | 可见性 | 作用范围 | Java 类比 |
|---|---|---|---|
大写 (如 GetHello、fmt.Println) |
导出(公开) | 可被其他包导入后访问 | public |
小写 (如 getHello) |
未导出(私有) | 仅当前包内可访问 | private(同包) |
举个例子:

pkg 包下的 main.go 如果要调用 cmd 包下面的 app.go 和 util.go 的方法,比如右边 GetHello2 就是 app.go 里面的方法,这种情况下需要将方法首字母大写,如果改成小写就会报错找不到定义的方法,因为此时的方法是包私有的,也就是只有 cmd 包下面的 go 文件可以调用这个方法。

下面就是在 util.go 中调用这个小写方法,可以看到是没有问题的。

1.4 Go 项目结构
下面是推荐的 Go 项目结构:
go
项目名/
├── go.mod # 模块声明
├── go.sum # 依赖哈希
├── main.go # 项目入口(package main,含 main())
├── cmd/ # 多可执行程序入口(可选)
│ └── app/ # 另一个可执行程序
│ └── main.go
├── pkg/ # 公共工具包(被多个模块引用)
│ └── calc/ # 包名calc,放工具函数
│ ├── add.go
│ └── sub.go
└── internal/ # 内部包(仅当前项目可导入,外部不可)
└── utils/
└── log.go
2. 注释
跟 Java 差不多,Go 的注释是下面两种,就不多说了:
//,单行注释/**/,多行注释
3. 函数
这里先对函数做一个简单的介绍,跟 java 一样,main 函数是一个可执行程序的入口,但是如果存在 init 方法,会先执行 init 方法,再执行 main 方法,如下面的例子(上一篇文章的初始化项目):
go
package main
import (
"fmt"
"go-learn-1/learn-1/pkg/cmd"
)
func init() {
fmt.Println("init")
}
func main() {
fmt.Println(cmd.GetHello1())
fmt.Println(cmd.GetHello2())
}
输出结果如下:
go
init
✅ Running as [pkg]: learn-1/pkg/cmd/util.go
✅ Running as [pkg]: learn-1/pkg/cmd/app.go
函数最简单的格式就是 func functionName(),() 中可以通过 , 传入参数,也可以在 () 后面定义返回的类型,但是注意,go 的返回值可以写多个,这个跟 Java 语法有点不一样,我们定义一个 math.go,里面包含一个 caculate 方法用于计算 +、-、*、/:
go
package cmd
func Caculate(a int, b int) (int, int, int, int) {
return a + b, a - b, a * b, a / b
}
在 main 方法中进行调用
go
package main
import (
"fmt"
"go-learn-1/learn-1/pkg/cmd"
)
func main() {
fmt.Println(cmd.Caculate(12, 3))
}
输出结果如下:
go
15 9 36 4
4. 类型
首先来看下类型总览:
| 类型大类 | 具体类型 | Java 类比 | 核心特点 |
|---|---|---|---|
| 基础类型 | 整数、浮点、布尔、字符串、复数 | 基本类型(int/boolean/String) | 均为值类型,无包装类 |
| 复合类型 | 数组、切片(slice)、映射(map)、结构体(struct)、通道(channel) | 数组、ArrayList、HashMap、类、无对应 | 组合多值,slice/map/channel 是引用类型 |
| 特殊类型 | 指针、接口、函数类型、error 类型 | 指针、接口、Lambda、Exception | Go 特有 / 差异大,实战高频 |
| 自定义类型 | type 关键字定义的新类型(如 type MyInt int) | 自定义类 / 枚举 | 类型隔离,可绑定方法 |
核心规则:Go 是静态类型语言(编译期确定类型)、强类型语言(无隐式类型转换,如 int 不能直接转 string)
4.1 基本类型
基本类型就是 Go 最底层的类型,全部是值类型,也就是方法传参的时候通过值传递,我们都知道 Java 方法传参的时候可以通过值传递 或者是引用传递 ,如果是对象传递的就是对象的引用,如果是基本数据类型就传的是值,同时 go 是没有包装类的。
4.1.1 数值类型(精准位数划分)
| 类型 | 说明 | 示例 | Java 类比 |
|---|---|---|---|
| int8/16/32/64 | 有符号整数(8-64 位) | var a int32 = 10 | byte/short/int/long |
| uint8/16/32/64 | 无符号整数(8-64 位) | var b uint8 = 255 | 无直接对应(需手动处理) |
| int/uint | 与平台位数一致(32/64 位) | var c int = 100 | int/long |
| float32/64 | 浮点数 | var d float64 = 3.14 | float/double |
| complex64/128 | 复数(极少用) | var e complex64 = 1+2i | 无直接对应 |
go
func baseTest() {
// 有符号整数
var a int32 = 10
fmt.Println("int32类型:", a)
// 输出 int32类型: 10
var b uint8 = 255
// b = 256 报错, 常量256超出uint8类型范围
fmt.Println("uint8类型:", b)
// 输出 uint8类型: 255
// 浮点型(优先用float64,精度更高)
var pi float64 = 3.1415926
fmt.Println("float64类型:", pi)
// 输出 float64类型: 3.1415926
}
4.2.2 布尔类型
| 类型 | 说明 | 示例 |
|---|---|---|
| bool | 仅 true/false,不能用 0/1 替代 | var isOk bool = true |
对于 go,不能将 1 复制给 bool 类型.
go
func boolTest() {
var isSuccess bool = true
var isFail bool = false
fmt.Println("成功:", isSuccess)
// 输出:成功: true
fmt.Println("失败:", isFail)
// 输出:失败: false
// 错误示例:Go不支持0/1替代bool
// var flag bool = 1 // 编译报错:无法将int类型赋值给bool类型
}
4.2.3 字符串类型
| 类型 | 说明 | 示例 |
|---|---|---|
| string | 不可变、UTF-8 编码(天然支持中文) | var s string = "你好 Go" |
下面是 string 的例子以及 string 比较常用的一些函数,💡 注意对于 go,中文的字节数是 3 字节。
go
func stringTest() {
// 天然支持中文(UTF-8编码)
var s string = "你好 Go"
fmt.Println("字符串内容:", s)
// 输出:字符串内容: 你好 Go
fmt.Println("字节数:", len(s))
// 输出:字节数: 7
// 字符串常用操作(strings包)
fmt.Println("是否包含Go:", strings.Contains(s, "Go"))
fmt.Println("替换:", strings.Replace(s, "Go", "Golang", 1)) // 输出:替换: 你好 Golang
}
4.2 复合类型
| 类型 | 定义示例 | 核心特点 | Java 类比 |
|---|---|---|---|
| 数组 N T | var arr 3int = 3int{1,2,3} | 值类型、长度固定(3int 和 4int 是不同类型) | 数组(但 Java 数组长度是属性) |
| 切片 \[\] T | slice := make(\[\]int, 3, 5) | 引用类型、动态扩容、基于数组创建 | ArrayList(更轻量,无额外方法) |
| 映射 map K V | m := make(mapstringint) m"age" = 20 | 引用类型、键需可比较、无序 | HashMap |
| 结构体 struct | type User struct { Name string json:"name" Age int } |
值类型、可绑定方法、支持标签 | 自定义类(无继承) |
| 通道 chan T | ch := make(chan int) | 引用类型、并发安全、goroutine 通信 | BlockingQueue(仅功能类比) |
4.2.1 数组
下面简单测试下这几种类型,后面等到后续的章节再一个一个细看,数组的创建语法给 java 还是不一样的,不需要 new,数组的遍历语法和 java 不一样:
java
func arrTest() {
var arr [3]int = [3]int{1, 2, 3}
fmt.Println("arr: ", arr)
fmt.Println("arr len: ", len(arr))
// 遍历数组
for i, v := range arr {
fmt.Printf("index: %d, value: %d\n", i, v)
}
// 输出结果
// arr: [1 2 3]
// arr len: 3
// index: 0, value: 1
// index: 1, value: 2
// index: 2, value: 3
}
4.2.2 切片
切片类型,切片类型类似 Java 的 ArrayList,基于数组来动态扩容,创建切片的方法有三种:
- 基于数组创建
- 直接使用 make 创建
💡 切片的获取元素方式和设置元素方式跟数组是一样的,同时这里的扩容不像 ArrayList 一样是扩容 1.5 倍。
go
func sliceTest() {
// 1. 基于数组来创建
arr := [3]int{1, 2, 3}
// 左闭右开: [1, 2]
slice1 := arr[0:2]
fmt.Println("slice1: ", slice1)
fmt.Println("slice1: ", len(slice1))
// 2. 使用 make 初始化
// 长度为 3 的切片, 初始值: [0, 0, 0], 容量是 5
slice2 := make([]int, 3, 5)
slice2[0] = 100
slice2[1] = 200
slice2[2] = 300
fmt.Println("slice2: ", slice2)
fmt.Println("slice2: ", len(slice2))
// 追加数据
slice2 = append(slice2, 400, 500)
fmt.Println("slice2: ", slice2)
fmt.Println("slice2: ", len(slice2))
// 继续追加, 动态扩容
slice2 = append(slice2, 600, 700)
fmt.Println("slice2: ", slice2)
fmt.Println("slice2: ", len(slice2))
}
输出结果如下:
go
slice1: [1 2]
slice1: 2
slice2: [100 200 300]
slice2: 3
slice2: [100 200 300 400 500]
slice2: 5
slice2: [100 200 300 400 500 600 700]
slice2: 7
4.2.3 map
下面就是用的比较多的 map 类型的创建,类比 Java 的 HashMap,map 的初始化有两种方式,可以通过 make 初始化,又或者通过字面量来初始化,获取值则是和数组方式差不多,只不过数组是传入下标获取值,而 map 则是传入 key 获取值,下面是简单的示例:
go
func mapTest() {
// 1. make 初始化
// key: string
// value: int
m1 := make(map[string]int)
m1["age"] = 20
m1["score"] = 35
fmt.Println("map: ", m1)
// 2. 字面量初始化, 可以提前初始化一些值进去
m2 := map[string]int{
"age": 21,
"score": 22,
}
fmt.Println("map: ", m2)
// 获取值
name1, ok := m1["age"]
if ok {
fmt.Println("age:", name1)
}
// 删除 key-value
delete(m1, "age")
fmt.Println("m1:", m1)
}
func main() {
mapTest()
}
输出结果如下:
go
map: map[age:20 score:35]
map: map[age:21 score:22]
age: 20
m1: map[score:35]
4.2.4 结构体
这部分就和 c 语言的语法差不多了,同样的 struct 就代表结构体的关键字,结构体可以类比 Java 的类,既然有类,那么同样类里面有方法,而我们直到 go 的结构体里面是不能定义方法的,但是 go 也可以用其他方式为结构体来绑定方法,例子如下:
go
type User struct {
Name string `json:"name"` // json 序列化的字段名
Age int `json:"age"`
IsStudent bool `json:"isStudent"`
}
// func 后面定义上结构体, 绑定方法
func (u User) GetInfo() string {
return fmt.Sprintf("name: %s, age: %d, isStudent: %v", u.Name, u.Age, u.IsStudent)
}
func main() {
// 初始化结构体
u1 := User{Name: "张三", Age: 18, IsStudent: true}
fmt.Println("结构体 u1 内容: ", u1)
fmt.Println(u1.GetInfo())
// 指针初始化, 避免复制大对象
u2 := &User{Name: "李四", Age: 20, IsStudent: false}
// 访问指针内容
fmt.Println("结构体 u2 内容: ", *u2)
fmt.Println(u2.GetInfo())
}
输出结果如下:
go
结构体 u1 内容: {张三 18 true}
name: 张三, age: 18, isStudent: true
结构体 u2 内容: {李四 20 false}
name: 李四, age: 20, isStudent: false
4.2.5 通道
创建通道,然后往里面写入数据,最后可以遍历通道获取里面的数据,先进先出:
go
func channelTest() {
// 创建有缓冲通道(容量2)
ch := make(chan int, 2)
// 发送数据到通道(goroutine中执行)
go func() {
ch <- 10
ch <- 20
close(ch) // 关闭通道
}()
// 接收通道数据
for num := range ch {
fmt.Println("通道接收数据: ", num)
// 10
// 20
}
}
4.3 特殊类型
| 类型 | 定义 / 示例 | 核心作用 | Java 类比 |
|---|---|---|---|
| 指针 *T | a := 10 p := &a // 取地址 *p = 20 // 解引用 | 指向内存地址,避免大对象复制 | 无显式指针(仅引用) |
| 接口 interface | type Greeting interface { SayHello() string } | 行为抽象、隐式实现、空接口接收任意类型 | 接口(但 Go 无需 implements 关键字) |
| 函数类型 | type CalculateFunc func(int, int) int func Add(a,b int) int { return a+b } | 函数是一等公民,可作为参数 / 返回值 | Lambda / 函数式接口 |
| error 类型 | func Divide(a,b int) (int, error) { if b==0 { return 0, fmt.Errorf("除数为 0") } return a/b, nil } | 错误处理核心,内置接口(仅 Error() 方法) | Exception(无 try-catch,靠 if err!=nil 处理) |
4.3.1 指针类型
指针类似 C 语言的指针,通过 & 取地址,然后通过 * 可以获取到地址里面的内容,下面是一个简单的例子,指针可以和基本数据类型、函数等等相结合,这里就先简单看下基本数据类型的例子:
go
func pointerTest() {
a := 10
p := &a // 取a的内存地址,p是 *int 类型
fmt.Println("a 的地址: ", p)
//
fmt.Println("a 的值", *p)
// 通过指针修改原变量值
*p = 20
fmt.Println(a)
}
输出如下:
go
a 的地址: 0xc00000a088
a 的值 10
20
4.3.2 接口
Go 的接口和 Java 的设计理念不同,Go 属于隐式实现,而 Java 属于显式实现,对于 Go,只要结构体实现了接口的所有方法,就自动满足接口,而对于 Java,必须明确声明 class X implements Interface。
⚠️ 有一点比较特殊的就是 Java 里面有 Object,而 Go 里面也有空接口,空接口可以赋值任何类型,通常作为顶层的实现,下面看一个简单的例子:
go
// 定义接口
type Getting interface {
SayHello() string
}
// 定义结构体, 类比 Java 的类, 实现接口
type Student struct {
Name string
}
type Teacher struct {
Name string
}
// 实现了 SayHello 接口
func (s Student) SayHello() string {
return "hello, " + s.Name
}
// 实现了 SayHello 接口
func (t Teacher) SayHello() string {
return "hello, " + t.Name
}
// 传入接口
func Greet(g Getting) string {
return "Greet: " + g.SayHello()
}
// 特殊: 空接口, 类比 Java 的 Object
func LogData(data interface{}) {
// 这里可以处理任何传入的数据
fmt.Printf("记录日志: %v\n", data)
}
func interfaceTest() {
stu := Student{
Name: "student",
}
fmt.Println(stu.SayHello())
tea := Teacher{
Name: "teacher",
}
fmt.Println(tea.SayHello())
fmt.Println(Greet(stu))
fmt.Println(Greet(tea))
LogData(1)
LogData("123")
LogData(stu)
}
func main() {
interfaceTest()
// hello, student
// hello, teacher
// Greet: hello, student
// Greet: hello, teacher
// 记录日志: 1
// 记录日志: 123
// 记录日志: {student}
}
Getting 就是接口名称,而结构体 Student 和 Teacher 实现了这个接口,Greet 方法需要传入接口,返回字符串,LogData 里面传入一个空接口,负责打印日志。
4.3.3 函数类型
函数类型和接口差不多,类似 Java 的函数式编程,举个例子:type Handler func(w http.ResponseWriter, r *http.Request),通过 func 来定义一个函数类型:
- ✅ 接口是不需要显示声明的,如果一个结构体实现了接口中定义的所有方法,它就自动实现了该接口。
- ✅ 函数类型本身就是一个类型,任何签名匹配的 函数 、方法 或 匿名函数(闭包) 都可以赋值给该类型的变量。
下面是函数类型的简单例子:
go
type ProcessFunc func(string) string
// 具体逻辑函数, 参数和返回值跟上面的 ProcessFunc 一样, 所以可以将 toUpper 赋值到 ProcessFunc
func toUpper(data string) string {
return strings.ToUpper(data)
}
// 具体使用
func Run(p ProcessFunc, s string) {
res := p(s)
fmt.Println(res)
}
func funcTest() {
// 直接调用函数
Run(toUpper, "hello")
// 传入匿名函数
Run(func(s string) string { return strings.ToLower(s) }, "HELLO")
}
func main() {
funcTest()
}
ProcessFunc 是定义的函数,传入一个 string 字符串,最终返回结果是 string 类型的字符串,Run 方法接受 ProcessFunc 类型的传参,在方法内部调用具体逻辑,toUpper 的传参和返回值和 ProcessFunc 一样,所以可以作为这个函数的参数,又或者像是第二种写法一样,直接传入匿名函数,匿名函数的参数和返回值要和定义的函数一致。
这种写法比 Java 方便,Java 中如果需要传入一个函数,就需要用到 Java8 的函数式编程,使用 Function 这些对象进行传参,而 go 则是可以直接把函数进行传递,如果只需要传递一段单一的逻辑,或者作为回调函数(Callback) ,这种情况下可以考虑函数式编程。
4.3.4 error 类型
go 内置包 builtin 中提供了 error 的相关定义,error 是一个接口,代码如下:
go
type error interface {
Error() string
}
也就是说所有实现了 Error() string 方法的类型都自动实现了 error 接口,可以作为错误使用,当然我们也可以直接使用 error 作为返回值,下面就来看下两种方式,分别是标准的错误返回以及我们自定义一个 error 类型。
go
// 1. 标准错误
func doErrorSimple() error {
return errors.New("something went wrong")
}
// 2. 自定义错误
type MyCustomError struct {
Code int
Message string
}
// 结构体实现 error 接口
func (e MyCustomError) Error() string {
return fmt.Sprintf("[Error Code %d]: %s", e.Code, e.Message)
}
// 返回自定义结构体
func doComplexThing() error {
return MyCustomError{Code: 500, Message: "something went wrong"}
}
func errorTest() {
if err := doErrorSimple(); err != nil {
fmt.Println("SimpleError: ", err)
}
// 获取自定义异常
if err := doComplexThing(); err != nil {
fmt.Println("ComplexThingError: ", err)
if customErr, ok := err.(MyCustomError); ok {
fmt.Println(customErr.Code, customErr.Message)
}
}
// 写法二, 指针写法
if err := doComplexThing(); err != nil {
fmt.Println("ComplexThingError: ", err)
var customErr MyCustomError
if errors.As(err, &customErr) {
fmt.Println(customErr.Code, customErr.Message)
}
}
}
func main() {
errorTest()
}
输出结果如下:
go
SimpleError: something went wrong
ComplexThingError: [Error Code 500]: something went wrong
500 something went wrong
ComplexThingError: [Error Code 500]: something went wrong
500 something went wrong
📌 在 Go 中,nil 表示 "没有错误",这是 Go 错误处理模式的基础:函数通常返回最后一个参数为 error,开发者需要检查是否为 nil,如果不为 nil 就说明没有发生错误,再继续执行下面的逻辑。
Go 提供了下面两个主要函数来创建 error:
| 函数 | 包 | 用途 | 示例 |
|---|---|---|---|
errors.New |
errors |
创建静态文本错误 | errors.New("file not found") |
fmt.Errorf |
fmt |
创建格式化文本错误(支持 %v, %w 等) |
fmt.Errorf("user %s not found", name) |
同时 Go 1.13+ 之后可以通过 %w 来包装 error,能够保留原始的错误消息,同时添加新的上下文,并且可以通过 errors.Is 和 errors.As 来检查底层错误。
go
func main() {
err := fmt.Errorf("failed to open file: %w", os.ErrNotExist)
// 检查是否包含底层错误
if errors.Is(err, os.ErrNotExist) {
fmt.Println(err)
fmt.Println("文件确实不存在")
// failed to open file: file does not exist
// 文件确实不存在
}
}
📚 Go 中没有 try-catch 这种语法,必须手动检查每个返回的 err 是否为 nil。
4.4 自定义类型
| 类型 | 定义示例 | 核心特点 |
|---|---|---|
| 自定义类型 | type MyInt int var a MyInt = 10 //a = 20 ❌ 类型不匹配 | 全新类型,与原类型隔离,需显式转换 |
| 类型别名 | type IntAlias = int var c IntAlias = 30 c = 40 ✅ | 仅别名,与原类型等价,无需转换 |
我们可以通过 type {typeName} {type} 来自定义类型,同时也可以为类型设置别名,来看下简单的例子:
go
package main
import "fmt"
// 自定义类型(全新类型)
type MyInt int
// 类型别名(仅别名)
type IntAlias = int
func main() {
// 自定义类型
var a MyInt = 10
var b int = 20
// a = b // 编译报错: 无法将int类型赋值给MyInt类型\
//显式转换后可赋值
a = MyInt(b)
// 自定义类型: 20
fmt.Println("自定义类型:", a)
// 类型别名
var c IntAlias = 30
var d int = 40
// 无需转换,直接赋值
c = d
fmt.Println("类型别名:", c)
// 类型别名: 40
}
5. 小结
这篇文章就简单看下 go 的基本语法,下一篇文章继续学习 go 的类型转换以及常用的数组,结构体...等类型的操作。
如有错误,欢迎指出!!!