1.环境安装
详情见我另外一篇文章
https://blog.youkuaiyun.com/2201_75669520/article/details/148639277?spm=1001.2014.3001.5502
2.GO语言结构
package main
import (
"fmt"
"example.com/GoCode/mathClass"
)
func main() {
/* 这是我的第一个简单的程序 */
fmt.Println("nihao!")
fmt.Println(mathClass.Add(1, 2))
fmt.Println(mathClass.Sub(1, 2))
}
- 第一行代码 package main 定义了包名。你必须在源文件中非注释的第一行指明这个文件属于哪个包,如:package main。package main表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包。
- 下一行 import “fmt” 告诉 Go 编译器这个程序需要使用 fmt 包(的函数,或其他元素),fmt 包实现了格式化 IO(输入/输出)的函数。
- 下一行 func main() 是程序开始执行的函数。main 函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有 init() 函数则会先执行该函数)。
- 下一行 /…/ 是注释,在程序执行时将被忽略。单行注释是最常见的注释形式,你可以在任何地方使用以 // 开头的单行注释。多行注释也叫块注释,均已以 /* 开头,并以 */ 结尾,且不可以嵌套使用,多行注释一般用于包的文档描述或注释成块的代码片段。
- 下一行 fmt.Println(…) 可以将字符串输出到控制台,并在最后自动增加换行字符 \n。
使用 fmt.Print(“hello, world\n”) 可以得到相同的结果。
Print 和 Println 这两个函数也支持使用变量,如:fmt.Println(arr)。如果没有特别指定,它们会以默认的打印格式将变量 arr 输出到控制台。 - 当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 protected )。
关于包,根据本地测试得出以下几点:
- 文件名与包名没有直接关系,不一定要将文件名与包名定成同一个。
- 文件夹名与包名没有直接关系,并非需要一致。
- 同一个文件夹下的文件只能有一个包名,否则编译报错。
可以看到,我上面的代码中,除了fmt之外,还导了一个我自己的包 “example.com/GoCode/mathClass”
package mathClass
// 添加两个整数
func Add(a, b int) int {
return a + b
}
package mathClass
// 两个整数相减
func Sub(a, b int) int {
return a - b
}
也就是这两个函数
一般来说,是创建一个新的文件夹,然后在里面写go程序
每个go程序第一行就需要定义包名,这个包名不一定要和文件夹名一致,但是通常会一致
导包的时候,不能只导mathClass,因为项目使用了Go模块,我们执行过 go mod init 命令,指定了模块路径
例如:
go mod init example.com/hello
因此,现在导包的包名就变成了 模块路径/子包名
“example.com/GoCode/mathClass”
但是我看过拉取下来的代码之后,发现里面的导包都不是这样的,应该是后续框架方面做了更改
3.基础语法
具体看菜鸟教程:https://www.runoob.com/go/go-basic-syntax.html
这里只标记我个人觉得关键的语法区别
3.1赋值
// 全局变量
var (
a int
b int
)
func main() {
a = 10
b = 5
// 局部变量
c := mathClass.Add(a, b)
d := mathClass.Sub(a, b)
fmt.Println(c)
fmt.Println(d)
}
使用 := 赋值操作符,这种赋值方式自动判断类型
但是只能在函数当中使用
函数之外的全局变量,通常通过
var (
a int
b int
)
这种方式声明
注意:
局部变量声明后必须使用
全局变量可以声明后不使用
常量赋值(不可改)
就是const,并且常量赋值后可以不使用
补充:%d和%s
用来拼接整形数字和字符串的
Go 语言中使用 fmt.Sprintf 或 fmt.Printf 格式化字符串并赋值给新串:
前者单纯赋值,后者赋值加输出
比如一个字符串长这样
“Code=%d&endDate=%s”
那么它%d和%s那里就可以插入一个整形和字符串
// 直接输出
fmt.Printf("Code=%d&endDate=%s", 123, "2021-12-31")
// 赋值
aaa := fmt.Sprintf("Code=%d&endDate=%s", 123, "2021-12-31")
fmt.Println(aaa)
当然,也有更简单的办法
fmt.Println("Hello","World",123)
3.2Go的运算符(指针)
基础运算符那些其实和Java不能说很像,只能说一模一样
唯一需要在意的就是Go多了个指针
但是不用望而生畏,Go的指针相较于C艹的简单多了,这里简单讲讲区别(因为原来也学过大半年C艹)
从运算上相比:Go的指针仅允许*和&操作,C艹可以做指针运算
从内存上相比:Go也是有GC的,C艹得自己管理
从使用上相比:Go只用来进行值传递和结构体修改,几乎不用多级指针,C艹运用很多,频繁使用多级指针
3.2.1. &
取地址符 : 获取变量的内存地址,返回一个指针类型
c := 20
fmt.Println(&c)
输出:0xc00010c098
3.2.2. *
解引用符 : 通过指针访问或修改其指向的值
c := 20
d := &c
fmt.Println(d)
fmt.Println(*d)
简单来说
指针:就是地址,a的指针,就是保存a的地址
&:获取一个值的指针/地址
*:通过一个指针/地址,获取到这个指针/地址所对应的值
容易混淆的点:
“*”如果放在一个指针前面,那么表示获取这个指针所指向的值
但是“*”如果放在一个type(类型)前面,用于指定变量是作为一个指针
例子如下:
var c *int
c = &a
*c = 10
fmt.Println(*c)
代码中的 c 表示一个int类型的指针变量,用来接收 a 的指针
通过 *c 去修改这个指针指向的值
这就是Go中指针的基本用法
3.2.3. Go指针的常见用法
- 传递大对象,避免值拷贝
type User struct {
Name string
Age int
Email string
}
func UpdateAgeByPointer(u *User, age int) {
u.Age = age // 直接修改原对象
}
func UpdateAgeByValue(u User, age int) {
u.Age = age // 只修改副本,原对象不受影响
}
func main() {
user1 := &User{Name: "Alice", Age: 30}
UpdateAgeByPointer(user1, 31)
fmt.Println(user1.Age) // 输出: 31
user2 := User{Name: "Bob", Age: 25}
UpdateAgeByValue(user2, 26)
fmt.Println(user2.Age) // 输出: 25
}
这里的 UpdateAgeByValue 就是类Java方式
但是Java当中,是隐式指针,也就是说,本质上还是 ByPointer 这种方式
Go因为有显式指针,可以我们自己选择是否使用。
运用指针的好处就是,修改的对象是之前的对象
不用指针,本质上是在 UpdateAgeByValue 这个方法里面进行了值拷贝
如果结构体(自建类)很大,那么值拷贝性能就很差了
如果就是想用值拷贝的方式,还想获得修改后的数据,那么就需要从函数里,返回新的对象
func UpdateAgeByValueWithReturn(u User, age int) User {
u.Age = age // 只修改副本,原对象不受影响
return u
}
func main() {
user3 := User{Name: "Charlie", Age: 40}
user3 = UpdateAgeByValueWithReturn(user3, 41)
fmt.Println(user3.Age) // 输出: 41
}
通过指针的方式创建结构体实例(常用)
func CreateUser(name string, age int, email string) *User {
return &User{
Name: name,
Age: age,
Email: email,
}
}
user3 := CreateUser("Charlie", 40, "charlie@example.com")
fmt.Println(*user3) // 输出: {Charlie 40 charlie@example.com}
这样创建实例返回的就是指针,本质上其实和Java当中的构造函数很像,是一种常用的创建实例方式
对比一下
func CreateUser(name string, age int, email string) *User {
return &User{
name,
age,
email,
}
}
func main() {
map1 := make(map[int] *User)
map1[1] = &User{"Alice", 25, "alice@example.com"}
map1[2] = CreateUser("Bob", 30, "bob@example.com")
}
其实结构体自带了构造方法,但是一般来说,为了可读性和可维护性,专门写一个Create方法是有必要的
一般来说,我们可以把具体的entity类写到专门的包下,和Java一样的规范,然后其他类导包就好了
package main
import (
"fmt"
"example.com/GoCode/entity"
)
func main() {
fmt.Println("Hello, world!")
user_map := make(map[int]*entity.User)
user_map[1] = entity.CreateUser(1, "Alice", 20, true)
user_map[2] = entity.CreateUser(2, "Bob", 25, false)
fmt.Println(*user_map[1])
fmt.Println(*user_map[2])
}
总结:
Java里自带隐式指针,之前被娇生惯养习惯了
现在用Go如果想达到之前Java的效果,就必须手动传指针进去
3.3条件和循环语句
3.3.1 条件语句
和Java一模一样
多了个select:类似于 switch 语句,但是select会随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。
3.3.2 循环语句
while和do while直接没了
只有for了(乐)
用法和Java一模一样
关键:
条件和循环语句的用法和Java一模一样,但是区别是不用加(),加了会报错!!!
3.4 函数
Go 语言函数定义格式如下:
func function_name( [parameter list] ) [return_types] {
函数体
}
- func:函数由 func 开始声明
- function_name:函数名称,参数列表和返回值类型构成了函数签名。
- parameter list:参数列表,参数就像一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为实际参数。参数列表指定的是参数类型、顺序、及参数个数。参数是可选的,也就是说函数也可以不包含参数。
- return_types:返回类型,函数返回一列值。return_types 是该列值的数据类型。有些功能不需要返回值,这种情况下 return_types 不是必须的。
没什么特别好说的,上面讲指针的时候就接触过
这里需要介绍几个新的点
3.4.1 函数可以返回多个值
func test(x int , y string) (int, string) {
return x, y
}
func main() {
x := 10
y := "hello"
c, d := test(x, y)
fmt.Println(c, d)
}
加个括号就行了,甚至可以返回不同类型
3.4.2 匿名函数
我也称之为,函数内部函数
package main
import "fmt"
func main() {
// 定义一个匿名函数并将其赋值给变量add
add := func(a, b int) int {
return a + b
}
// 调用匿名函数
result := add(3, 5)
fmt.Println("3 + 5 =", result)
// 在函数内部使用匿名函数
multiply := func(x, y int) int {
return x * y
}
product := multiply(4, 6)
fmt.Println("4 * 6 =", product)
// 将匿名函数作为参数传递给其他函数
calculate := func(operation func(int, int) int, x, y int) int {
return operation(x, y)
}
sum := calculate(add, 2, 8)
fmt.Println("2 + 8 =", sum)
// 也可以直接在函数调用中定义匿名函数
difference := calculate(func(a, b int) int {
return a - b
}, 10, 4)
fmt.Println("10 - 4 =", difference)
}
3.4.3 方法
方法是要绑定结构体(Java中的类)的
本质上就是给结构体配置的函数
特点就是可以调用结构体里的成员(Java中类的属性)
package main
import (
"fmt"
)
/* 定义结构体 */
type Circle struct {
radius float64
}
func main() {
var c1 Circle
c1.radius = 10.00
fmt.Println("圆的面积 = ", c1.getArea())
}
//该 method 属于 Circle 类型对象中的方法
func (c Circle) getArea() float64 {
//c.radius 即为 Circle 类型对象中的属性
return 3.14 * c.radius * c.radius
}
3.5 数组
3.5.1 基操
和Java有点区别,但不大(感觉每个语言就喜欢整点不一样的,统一下不好吗)
var arrayName [size]dataType
// 声明
var balance [10]float32
// 初始化,像上面那样,不赋值就默认都是0
// 赋值,肯定还是 := 最方便啦
numbers := [5]int{1, 2, 3, 4, 5}
// 不确定长度可以...代替
balance := [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
// 可以只初始化其中某几个元素
balance := [5]float32{1:2.0, 3:7.0}
// 访问,和java一样
numbers := [5]int{1, 2, 3, 4, 5}
n := numbers[2]
fmt.Println(n)
总的来说,新奇一点的地方就是可以初始化的时候单独赋值其中某几个元素了
3.5.2 多维数组和函数插入
多维没什么好说的,多加一个[]的事情
package main
import "fmt"
func main() {
// 创建二维数组
sites := [2][2]string{}
// 向二维数组添加元素
sites[0][0] = "Google"
sites[0][1] = "Runoob"
sites[1][0] = "Taobao"
sites[1][1] = "Weibo"
// 显示结果
fmt.Println(sites)
}
函数插入数组
func myFunction(param [10]int) {
....
}
// 如果想要修改原数组的值,插入指针就好了
func myFunction2(param *[10]int) {
....
}
到这里做个小总结
Go的语法和Java的相比,其实就是把很多地方反过来了
例如:
int a = 10;
int b;
b = 10;
int[] arr = new int[10]
a := 10
var b int = 10
arr := [10]int
说实话,Go这种是有点反人类的
3.6 结构体
与类类似,但是很多地方不一样
最大的区别在于
结构体的作用是数据封装,没有方法实现
本质上是创建了一个新的数据类型给我们使用,如果要给当前结构体设置方法,参照 3.4.3
type struct_variable_type struct {
member definition
member definition
...
member definition
}
至于结构体的成员访问,调用,结构体传入函数,结构体指针相关的内容
之前在指针那一款就演示过了
直接把它当作自定义的一个新数据类型用就完事了
3.7 切片
简单来说就是没有长度设置的数组,或者说动态数组
3.7.1 定义和初始化
// 直接创建一个没有设置长度的数组
numbers2 := []int {1, 2, 3, 4, 5}
m := numbers2[2]
fmt.Println(m)
// 或者用make更直观一点,len表示初始长度
slice1 := make([]type, len)
// cap表示最大长度,是可选参数
make([]T, length, capacity)
3.7.2 cap和len函数
cap就是获取切片的capacity最大容量
len就是获取切片的现存长度
3.7.3 切片截取
/* 创建切片 */
numbers := []int{0,1,2,3,4,5,6,7,8}
numbers2 := numbers[1:3]
切片numbers
截取就是numbers[l,r],截取[l,r)的元素,左闭右开。类比Java终端SubString函数
3.7.4 append和copy函数
append就是往切片里面加新的元素,同时也要让当前变量来接收
copy就是把老切片的元素,放到新切片
append的底层逻辑也用到了copy,下面这个是我手写的append方法
内置的append方法len和cap相同时,cap只会+1,我写的是翻倍
func append(s []int, x int) []int {
if len(s) == cap(s) {
newSlice := make([]int, len(s), len(s)*2)
copy(newSlice, s)
s = newSlice
}
s = s[:len(s)+1]
s[len(s)-1] = x
return s
}
本质上创建了一个新的切片来装更多的元素,没Java当中的list集合方便
3.8 range
和Java的foreach类似吧
搭配for循环使用的一种遍历工具
基础格式如下
for a,b := range arr {
fmt.Println(a,b)
}
其中的 a 和 b 在遍历的时候,面对不同类型的arr,代表的也不一样
a和b的名字可以随意取
面对 数组/切片/字符串时
a是索引下标(一般用i表示),b是下标对应的元素(一般用v表示)
面对map集合时
a是key,b是value
所以说用range遍历map的时候,一般都如下
for key,value := range map{
fmt.Println("key = ", key, " value = ", value)
}
还有一个就是忽略索引和忽略值
遍历的时候是可以忽略索引/忽略值的
package main
import "fmt"
func main() {
nums := []int{2, 3, 4}
// 忽略索引
for _, num := range nums {
fmt.Println("value:", num)
}
// 忽略值
for i := range nums {
fmt.Println("index:", i)
}
}
3.9 Map集合
func main() {
user_map := make(map[int]*entity.User)
user_map[1] = entity.CreateUser(1, "Alice", 20, true)
user_map[2] = entity.CreateUser(2, "Bob", 25, false)
fmt.Println(*user_map[1])
fmt.Println(*user_map[2])
v1 := user_map[1]
v2 := user_map[2]
v1.Name = "Alice1"
v2.Name = "Bob1"
fmt.Println(*v1)
fmt.Println(*v2)
delete(user_map, 1)
for key, value := range user_map {
fmt.Println(key, *value)
}
}
user_map := make(map[int]*entity.User)
[]中的是key的类型,后面是value的类型
调用那些和数组差不多
delete的用法就是 集合名+key
3.10 接口
接口本身的用处和定义就不过多阐述了,都是Java转的了,接口用的还少吗(乐)
3.10.1 基本结构
/* 定义接口 */
type interface_name interface {
method_name1 [return_type]
method_name2 [return_type]
method_name3 [return_type]
...
method_namen [return_type]
}
/* 定义结构体 */
type struct_name struct {
/* variables */
}
/* 实现接口方法 */
func (struct_name_variable struct_name) method_name1() [return_type] {
/* 方法实现 */
}
...
func (struct_name_variable struct_name) method_namen() [return_type] {
/* 方法实现*/
}
type Shape interface {
Area() float64
Perimeter() float64
}
type Circle struct {
radius float64
}
func (c Circle) Area() float64 {
return 3.14 * c.radius * c.radius
}
func (c Circle) Perimeter() float64 {
return 2 * 3.14 * c.radius
}
func main() {
c := Circle{radius: 5}
fmt.Println(c.Area())
fmt.Println(c.Perimeter())
}
3.10.2 空接口
所有类型的超集,所有类型都实现了空接口
主要用于需要传入或存储任意类型的场景:泛型容器、通用参数等
空接口:interface{}
要知道空接口的实例用法,看下一章的类型转换
3.11 类型转换
3.11.1 基本格式
除了整形转浮点型,可以直接类似Java那样转之外,其他类型转换都需要用到一个包:strconv
需要调用这个包当中的方法,进行类型转换
下面列出转换的格式
import (
"fmt"
"strconv"
)
func main() {
num := 123
str := strconv.Itoa(num)
fmt.Printf("整数 %d 转换为字符串为:'%s'\n", num, str)
num2, err := strconv.Atoi(str)
if err!= nil {
fmt.Println("字符串转换为整数失败:", err)
} else {
fmt.Printf("字符串 '%s' 转换为整数为:%d\n", str, num2)
}
}
这里就可以看到两种不同形式的语法
因为大部分类型转换的时候,都有可能发生错误,因此strconv中的方法在转型时,都会返回两个值:转换后的值,错误
第一种:整形转字符串
不可能发生错误,因此只有一个返回值
第二种:字符串转整形
可能发生错误,因此标准格式是,接收后判断是否有错,然后再使用转型后的值
还有其他很多类型转换的方法,可以写业务时问AI即可
3.11.2 接口类型转换
接口的类型转换分为两种形式:类型断言和类型转换
类型断言
还是先看格式
value.(type)
或者
value.(T)
value 是接口类型的变量,type 或 T 是要转换成的类型。
实例
package main
import "fmt"
func main() {
var i interface{} = "Hello, World"
str, ok := i.(string)
if ok {
fmt.Printf("'%s' is a string\n", str)
} else {
fmt.Println("conversion failed")
}
}
简单来讲,就是和strconv的类型转换一样,转换的时候,会给转换后的值和一个判断是否成功的条件
因此格式也和strconv的一样
先判断是否转换成功
再使用转换后的值
类型转换(接口)
格式
T(value)
这个其实就是Java当中常见的类型转换了
T 是目标接口类型,value 是要转换的值。
这种方式用的很少,也就是我之前说的,整形转字符串,byte转字符串这种可以用
实际运用
接口类型转换的实际运用其实要集合两个东西:多态和空接口
多态运用
package main
import "fmt"
type Shape interface {
Area() float64
}
type Rectangle struct {
width, height float64
}
type Circle struct {
radius float64
}
func (r Rectangle) Area() float64 {
return r.width * r.height
}
func (c Circle) Area() float64 {
return 3.14 * c.radius * c.radius
}
func printShapeInfo(s Shape) {
fmt.Printf("面积: %.2f\n", s.Area())
// 使用类型转换区分不同的具体类型
switch v := s.(type) {
case Circle:
fmt.Printf("这是一个圆,半径: %.2f\n", v.radius)
case Rectangle:
fmt.Printf("这是一个矩形,宽: %.2f, 高: %.2f\n", v.width, v.height)
default:
fmt.Println("未知形状")
}
}
func main() {
var s1 Shape = Circle{10}
var s2 Shape = Rectangle{10, 20}
printShapeInfo(s1)
printShapeInfo(s2)
}
可以去判断当前接口被实现的类型,然后做区分
空接口运用
package main
import "fmt"
func processValue(v interface{}) {
// 判断 v 是否为字符串类型
if str, ok := v.(string); ok {
fmt.Printf("接收到字符串: %s\n", str)
}
// 判断 v 是否为整数类型
if num, ok := v.(int); ok {
fmt.Printf("接收到整数: %d\n", num)
}
}
func main() {
processValue("hello") // 输出: 接收到字符串: hello
processValue(42) // 输出: 接收到整数: 42
processValue(3.14) // 无输出,因为未匹配任何类型
}
因为空接口是个超集,所以类都是它的实现类,因此可以传进去单独判断
本质上也是多态运用
379

被折叠的 条评论
为什么被折叠?



