Golang:简介、基本语法、函数、defer、Test功能

春招找实习告一段落了,好长时间没更CSDN的博客,期间写的一些笔记用 typora + git 直接推到github里面了,就没在CSDN里再发了,我的github:https://github.com/WangGarrison
本篇博客简单记录一下最近学的golang

Go语言简介

Go语言特性

  • 静态强类型、编译型、并发型
  • 具有垃圾回收功能
  • 无类和继承,通过接口实现多态
  • 不支持自定义的泛型类型
  • 将“++”、“–”从运算符降级为语句
  • 保留指针,但默认阻止指针运算
  • 将切片和字典作为内置类型,从运行时的层面进行优化
  • Go语言的源码无须头文件,编译的文件都来自于后缀名为.go的源码文件。
  • 语句结尾不写分号,写了一些编译器保存时会自动去掉
  • 注释方式和C/C++相同

编译型语言

  • Go 使用编译器来编译代码。编译器将源代码编译成二进制(或字节码)格式;在编译代码时,编译器检查错误、优化性能并输出可在不同平台上运行的二进制文件。要创建并运行 Go 程序,程序员必须执行如下步骤:
    • 使用文本编辑器创建 Go 程序并保存
    • 编译
    • 运行编译得到的可执行文件
  • Go 自带了编译器,因此无须单独安装编译器

在Go语言出现之前,开发者们总是面临非常艰难的抉择,究竟是使用执行速度快但是编译速度并不理想的语言(如:C++),还是使用编译速度较快但执行效率不佳的语言(如:.NET、Java),或者说开发难度较低但执行速度一般的动态语言呢?显然,Go语言在这 3 个条件之间做到了最佳的平衡:快速编译,高效执行,易于开发

Go语言支持交叉编译,比如说你可以在运行 Linux 系统的计算机上开发可以在 Windows 上运行的应用程序。这是第一门完全支持 UTF-8 的编程语言,这不仅体现在它可以处理使用 UTF-8 编码的字符串,就连它的源码文件格式都是使用的 UTF-8 编码。Go语言做到了真正的国际化!

编译原理

  • 词法与语法分析
  • 类型检查
  • 中间代码生成
  • 机器码生成

目录结构

  • 一个Go语言项目的目录一般包含以下三个子目录:
    • src 目录:放置项目和库的源文件;
    • pkg 目录:放置编译后生成的包/库的归档文件;
    • bin 目录:放置编译后生成的可执行文件。
  • 源文件:
    • 命令源文件:如果一个 Go 源文件被声明属于 main 包,并且该文件中包含 main 函数,则它就是命令源码文件。命令源文件属于程序的入口,可以通过Go语言的go run 命令运行或者通过go build 命令生成可执行文件。
    • 库源文件:库源文件则是指存在于某个包中的普通源文件,并且库源文件中不包含 main 函数。

Go语法特性

1:for循环的条件不带圆括号

  • for 两边的括号被去掉,int 声明被简化为:=,直接通过编译器右值推导获得 a 的变量类型并声明。

     for i := 0;i<10;i++{
         //循坏代码
     }
    

2:if表达式条件不带原括号

 if 表达式{
	      //表达式成立执行的语句
}

3:强制的代码风格

  • 左括号必须紧接着语句不换行,其他样式的括号将被视为代码编译错误
  • 一些Go语言的开发环境或者编辑器在保存时,都会使用格式化工具对代码进行格式化,让代码提交时已经是统一格式的代码。

4:i++

  • 在Go语言中,自增操作符不再是一个操作符,而是一个语句。因此,在Go语言中自增只有一种写法:

  • i++
    
  • 如果写成前置自增++i,或者赋值后自增a=i++都将导致编译错误。

Hello World

创建一个hello.go文件,输入如下代码:

package main   // 声明main包

import "fmt"  // 导入 fmt 包,打印字符串是需要用到

func main() { 
    fmt.Println("Hello World")
}

运行代码:

  • 执行代码:go run hello.go
  • 生成二进制文件,再执行二进制文件:go bulid hello.go ./hello
  • VSCode点击run,如果提示go. mod file not find,执行go env -w GO111MODULE=auto后再点击run

go run与go build:

  • go run:编译并运行程序,但不会产生exe文件,运行速度也相应较慢
  • go build:编译不运行,生成exe文件

package

**package:**Go语言以“包”作为管理单位,每个 Go 源文件必须先声明它所属的包,所以我们会看到每个 Go 源文件的开头都是一个 package 声明,格式如下:

package name

其中 package 是声明包名的关键字,name 为包的名字。

Go语言的包与文件夹是一一对应的,它具有以下几点特性:

  • 一个目录下的同级文件属于同一个包。
  • 包名可以与其目录名不同。
  • main 包是Go语言程序的入口包,一个Go语言程序必须有且仅有一个 main 包。如果一个程序没有 main 包,那么编译时将会出错,无法生成可执行文件。

import

在包声明之后,是 import 语句,用于导入程序中所依赖的包,导入的包名使用双引号""包围,格式如下:

import "name"

其中 import 是导入包的关键字,name 为所导入包的名字。

代码第 4 行导入了 fmt 包,这行代码会告诉 Go 编译器,我们需要用到 fmt 包中的函数或者变量等,fmt 包是Go语言标准库为我们提供的,用于格式化输入输出的内容(类似于C语言中的 stdio.h 头文件)

也可以使用一个 import 关键字导入多个包,此时需要用括号( )将包的名字包围起来,并且每个包名占用一行,也就是写成下面的样子:

import(
    "name1"
    "name2"
)

main函数

main 函数,它是Go语言程序的入口函数,也即程序启动后运行的第一个函数。main 函数只能声明在 main 包中,不能声明在其他包中,并且,一个 main 包中也必须有且仅有一个 main 函数。这点和C/C++是类似的

main 函数是自定义函数的一种,在Go语言中,所有函数都以关键字 func 开头的,定义格式如下所示:

func 函数名 (参数列表) (返回值列表){
  函数体
}

fmt.Println(“Hello World”)

Println 是 fmt 包中的一个函数,它用来格式化输出数据,比如字符串、整数、小数等,类似于C语言中的 printf 函数

注意,Println 函数打印完成后会自动换行,ln是 line 的缩写。(Print不自动换行)

点号.是Go语言运算符的一种,这里表示调用 fmt 包中的 Println 函数。

Go语言基本语法

定义基本类型

go基本类型有:

  • bool(布尔值并不会隐式转换为数字值 0 或 1,反之亦然,必须使用 if 语句显式的进行转换,Go语言中不允许将整型强制转换为布尔型)
  • string // len(str)可以获取一个字符串的长度,支持下标索引访问,支持拼接s := s1 + s2,支持+=,``可以定义多行字符串
  • int、int8、int16、int32、int64,分别对应平台字节、8、16、32、64 bit大小的有符号整数
  • uint、uint8、uint16、uint32、uint64、uintptr
  • byte // uint8 的别名,代表了 ASCII 码的一个字符
  • rune // int32 的别名 代表一个 Unicode 码,当需要处理中文、日文或者其他复合字符用到
  • float32、float64
  • complex64(32位实数和虚数)、complex128(64位实数和虚数) //复数

哪些情况下使用int,哪些情况使用int8、int16等

int:程序逻辑对整型范围没有特殊需求。例如,对象的长度使用内建 len() 函数返回,这个长度可以根据不同平台的字节长度进行变化

int8等:在二进制传输、读写文件的结构描述时,为了保持文件的结构不会受到不同编译目标平台字节长度的影响,不要使用 int 和 uint,使用指定bit的int8等

声明变量

// 声明一个变量
var a int

// 编译器自动推导类型
var a = 100

//声明多个变量
var (
	a int
	b string
	c []float32
    d func() bool
    e struct {
        x int
    }
)

// 简短方式
名字 := 表达式
i,j := 0,1

/*
需要注意的是,简短模式(short variable declaration)有以下限制:
- 定义变量,同时显式初始化。
- 不能提供数据类型。
- 只能用在函数内部。

因为简洁和灵活的特点,简短变量声明被广泛用于大部分的局部变量的声明和初始化。var 形式的声明语句往往是用于需要显式指定变量类型地方,或者因为变量稍后会被重新赋值而初始值无关紧要的地方。
*/

Go语言和许多编程语言不同,它在声明变量时将变量的类型放在变量的名称之后。这样做的好处就是可以避免像C语言中那样含糊不清的声明形式,例如:int* a, b; 。其中只有 a 是指针而 b 不是。如果你想要这两个变量都是指针,则需要将它们分开书写。而在 Go 中,则可以和轻松地将它们都声明为指针类型:

var a,b *int  //a和b都是整型指针

当一个变量被声明之后,系统自动赋予它该类型的零值:int 为 0,float 为 0.0,bool 为 false,string 为空字符串,指针为 nil 等。所有的内存在 Go 中都是经过初始化的

变量的命名规则遵循骆驼命名法,即首个单词小写,每个新单词的首字母大写,例如:numShips 和 startDate 。

浮点数声明

浮点数在声明的时候可以只写整数部分或者小数部分,像下面这样:

纯文本复制
const e = .71828 // 0.71828
const f = 1.     // 1

很小或很大的数最好用科学计数法书写,通过 e 或 E 来指定指数部分:

const Avogadro = 6.02214129e23  // 阿伏伽德罗常数
const Planck   = 6.62606957e-34 // 普朗克常数

用 Printf 函数打印浮点数时可以使用“%f”来控制保留几位小数

fmt.Printf("%.2f\n", math.Pi)  //保留两位小数

复数声明

声明复数的语法格式如下所示:

var name complex128 = complex(x, y)name := complex(x, y)

其中 name 为复数的变量名,complex128 为复数的类型,“=”后面的 complex 为Go语言的内置函数用于为复数赋值,x、y 分别表示构成该复数的两个 float64 类型的数值,x 为实部,y 为虚部。

对于一个复数z := complex(x, y),可以通过Go语言的内置函数real(z) 来获得该复数的实部,也就是 x;通过imag(z) 获得该复数的虚部,也就是 y

复数也可以用==!=进行相等比较,只有两个复数的实部和虚部都相等的时候它们才是相等的

变量的初始化、多赋值问题

var 变量名 类型 = 表达式
var a int = 10

var a = 10

a := 10
//注意:由于使用了:=,而不是赋值的等,因此推导声明写法的左值变量必须是没有定义过的变量。若定义过,将会发生编译错误

conn, err := net.Dial("tcp", "127.0.0.1:8080")
conn2, err := net.Dial("tcp", "127.0.0.1:8080")
/*
net.Dial 提供按指定协议和地址发起网络连接,这个函数有两个返回值,一个是连接对象(conn),一个是错误对象(err)
注意:在多个短变量声明和赋值中,至少有一个新声明的变量出现在左值中,即便其他变量名可能是重复声明的,编译器也不会报错
*/

多赋值问题

  • 对左侧操作数中的表达式,索引值进行计算和确定,首先确定左侧的操作数的地址,然后对右侧的赋值表达式进行计算,如果发现右侧的表达式计算引用左侧的变量,则创建临时变量进行值拷贝,最后完成计算

  • 从左到右的顺序依次计算

  • 例如使用go交换两个数字:

    //写法一:
    var a int = 100
    var b int = 200
    var t int
    t = a
    a = b
    b = t
    
    //写法二:
    var a int = 100
    var b int = 200
    b, a = a, b  //多重赋值时,变量的左值和右值按从左到右的顺序赋值。先算等号右边的值,把a的值存为t1,把b的值存为t2,然后把t1赋值给变量b,t2赋值给变量a
      ```
    
    
    

匿名变量

在编码过程中,可能会遇到没有名称的变量、类型或方法。虽然这不是必须的,但有时候这样做可以极大地增强代码的灵活性,这些变量被统称为匿名变量。

匿名变量的特点是一个下画线“”,“”本身就是一个特殊的标识符,被称为空白标识符。它可以像其他标识符那样用于变量的声明或赋值(任何类型都可以赋值给它),但任何赋给这个标识符的值都将被抛弃,因此这些值不能在后续的代码中使用,也不可以使用这个标识符作为变量对其它变量进行赋值或运算。使用匿名变量时,只需要在变量声明的地方使用下画线替换即可。例如:

func GetData() (int, int) {
	return 100, 200
}

func main() {
	a, _ := GetData()
	fmt.Print(a)
}

匿名变量不占用内存空间,不会分配内存。匿名变量与匿名变量之间也不会因为多次声明而无法使用。

变量作用域与生命周期

变量作用域概念与C/C++类似

  • 函数内定义的变量称为局部变量

  • 函数外定义的变量称为全局变量:全局变量声明必须以 var 关键字开头,如果想要在外部包中使用全局变量的首字母必须大写

  • 函数定义中的变量称为形式参数

  • Go语言程序中全局变量与局部变量名称可以相同,但是函数体内的局部变量会被优先考虑。

变量的生命周期指的是在程序运行期间变量有效存在的时间间隔。

  • 全局变量:它的生命周期和整个程序的运行周期是一致的
  • 局部变量:它的生命周期则是动态的,从创建这个变量的声明语句开始,到这个变量不再被引用为止
  • 形式参数和函数返回值:它们都属于局部变量,在函数被调用的时候创建,函数调用结束后被销毁

数据类型转换

Go语言不存在隐式类型转换,因此所有的类型转换都必须显式的声明:

注意:C中强制类型转换是(int),go中是int()

valueOfTypeB = typeB(valueOfTypeA)
a := 5.0
b := int(a)

只有相同底层类型的变量之间可以进行相互转换(如将 int16 类型转换成 int32 类型),不同底层类型的变量相互转换时会引发编译错误(如将 bool 类型转换为 int 类型)

浮点数在转换为整型时,会将小数部分去掉,只保留整数部分。

指针

Go语言为程序员提供了控制数据结构指针的能力,但是,并不能进行指针运算

指针(pointer)在Go语言中可以被拆分为两个核心概念:

  • 类型指针,允许对这个指针类型的数据进行修改,传递数据可以直接使用指针,而无须拷贝数据,类型指针不能进行偏移和运算。
  • 切片,由指向起始元素的原始指针、元素数量和容量组成。

受益于这样的约束和拆分,Go语言的指针类型变量即拥有指针高效访问的特点,又不会发生指针偏移,从而避免了非法修改关键性数据的问题。同时,垃圾回收也比较容易对不会发生偏移的指针进行检索和回收。

ptr := &v    // v 的类型为T,ptr 的类型为*T
var a int = 10
var ptr = &a
var ptr2 *int = &a
fmt.Printf("%T\n", ptr)  // *intfmt.Printf("%d\n", *ptr)  // 10
*ptr2 = 20
fmt.Print(a)  // 20

取地址操作符&和取值操作符*是一对互补操作符,&取出地址,*根据地址取出地址指向的值。和C中用法相同

创建指针的另一种方法——new() 函数

str := new(string)
*str = "hello"

fmt.Println(*str)

new() 函数可以创建一个对应类型的指针,创建过程会分配内存,被创建的指针指向默认值。

变量逃逸分析Escape Analysis

变量逃逸分析(Escape Analysis)——自动决定变量分配方式,提高运行效率

堆和栈各有优缺点,该怎么在编程中处理这个问题呢?在 C/C++ 语言中,需要开发者自己学习如何进行内存分配,选用怎样的内存分配方式来适应不同的算法需求。比如,函数局部变量尽量使用栈,全局变量、结构体成员使用堆分配等。程序员不得不花费很长的时间在不同的项目中学习、记忆这些概念并加以实践和使用。

Go语言将这个过程整合到了编译器中,命名为“变量逃逸分析”。通过编译器分析代码的特征和代码的生命周期,决定应该使用堆还是栈来进行内存分配。

编译器觉得变量应该分配在堆和栈上的原则是:

  • 变量是否被取地址;
  • 变量是否发生逃逸。

Go逃逸分析最基本的原则是:如果一个函数返回对一个变量的引用,那么它就会发生逃逸。

简单来说,编译器会分析代码的特征和代码生命周期,Go中的变量只有在编译器可以证明在函数返回后不会再被引用的,才分配到栈上,其他情况下都是分配到堆上。

指针逃逸:当一个对象的指针被多个方法或线程引用时,我们称这个指针发生了逃逸。

逃逸分析:在编译原理中,分析指针动态范围的方法称之为逃逸分析。更简单来说,逃逸分析决定一个变量是分配在堆上还是分配在栈上。

Go变量逃逸分析

逃逸实例:

/* 在程序的编译阶段,编译器会根据实际情况自动选择在栈或者堆上分配局部变量的存储空间,不论使用 var 还是 new 关键字声明变量都不会影响编译器的选择。*/
var global *int
func f() {
    var x int
    x = 1
    global = &x
}
func g() {
    y := new(int)
    *y = 1
}

函数 f 里的变量 x 必须在堆上分配,因为它在函数退出后依然可以通过包一级的 global 变量找到,虽然它是在函数内部定义的。用Go语言的术语说,这个局部变量 x 从函数 f 中逃逸了

相反,当函数 g 返回时,变量 *y 不再被使用,也就是说可以马上被回收的。因此,*y 并没有从函数 g 中逃逸,编译器可以选择在栈上分配 *y 的存储空间,也可以选择在堆上分配,然后由Go语言的 GC(垃圾回收机制)回收这个变量的内存空间

在实际的开发中,并不需要刻意的实现变量的逃逸行为,因为逃逸的变量需要额外分配内存,同时对性能的优化可能会产生细微的影响

常量

const name [type] = value
const pi int = 3.14
const pi = 3.14

常量的值必须是能够在编译时就能够确定的,可以在其赋值表达式中涉及计算过程,但是所有用于计算的值必须在编译期间就能获得。

  • 正确的做法:const c1 = 2/3
  • 错误的做法:const c2 = getNumber() // 引发构建错误: getNumber() 用做值

和变量声明一样,可以批量声明多个常量:

const (
	e = 2.71
	pi = 3.14
)

因为常量的值是在编译期就确定的,因此常量可以是构成类型的一部分,例如用于指定数组类型的长度:

const LEN = 10
var arr[LEN] int

如果是批量声明的常量,除了第一个外其它的常量右边的初始化表达式都可以省略,如果省略初始化表达式则表示使用前面常量的初始化表达式,对应的常量类型也是一样的。例如:

const (
    a = 1
    b
    c = 2
    d
)
fmt.Println(a, b, c, d) // 1 1 2 2

iota 常量生成器

  • 常量声明可以使用 iota 常量生成器初始化,它用于生成一组以相似规则初始化的常量,但是不用每行都写一遍初始化表达式。在一个 const 声明语句中,在第一个声明的常量所在的行,iota 将会被置为 0,然后在每一个有常量声明的行加一。
type Weekday int
const (
    Sunday Weekday = iota
    Monday
    Tuesday
    Wednesday
    Thursday
    Friday
    Saturday
)
//周日将对应 0,周一为 1,以此类推。

无类型常量

Go语言的常量有个不同寻常之处。虽然一个常量可以有任意一个确定的基础类型,例如 int 或 float64,或者是类似 time.Duration 这样的基础类型,但是许多常量并没有一个明确的基础类型。

编译器为这些没有明确的基础类型的数字常量提供比基础类型更高精度的算术运算,可以认为至少有 256bit 的运算精度。这里有六种未明确类型的常量类型,分别是无类型的布尔型、无类型的整数、无类型的字符、无类型的浮点数、无类型的复数、无类型的字符串。

通过延迟明确常量的具体类型,不仅可以提供更高的运算精度,而且可以直接用于更多的表达式而不需要显式的类型转换。

例如:math.Pi 无类型的浮点数常量,可以直接用于任意需要浮点数或复数的地方:

var x float32 = math.Pi
var y float64 = math.Pi
var z complex128 = math.Pi

模拟枚举

Go语言现阶段没有枚举类型,但是可以使用const常量与 iota 来模拟枚举类型,示例如下:

type Weekday int
const (
    Sunday Weekday = iota
    Monday
    Tuesday
    Wednesday
    Thursday
    Friday
    Saturday
)
//周日将对应 0,周一为 1,以此类推。

// 输出所有枚举值
fmt.Println(Sunday, Monday, Tuesday,)

//使用枚举类型并赋初值
var today Weekday = Sunday
fmt.Println(today)  //0

当然,iota 不仅可以生成每次增加 1 的枚举值。还可以利用 iota 来做一些强大的枚举常量值生成器。下面的代码可以方便的生成标志位常量:

const (    
    FlagNone = 1 << iota  //每次将上一次的值左移一位(二进制位),以得出每一位的常量值
    FlagRed
    FlagGreen
    FlagBlue
)

fmt.Printf("%d %d %d\n", FlagRed, FlagGreen, FlagBlue)  //2 4 8 (10进制)
fmt.Printf("%b %b %b\n", FlagRed, FlagGreen, FlagBlue)  //10 100 1000(2进制)

代码输出如下:

2 4 8
10 100 1000

类型别名type

类型别名是 Go 1.9 版本添加的新功能,主要用于解决代码升级、迁移中存在的类型兼容性问题。在 C/C++语言中,代码重构升级可以使用宏快速定义一段新的代码,Go语言中没有选择加入宏,而是解决了重构中最麻烦的类型名变更问题。

在 Go 1.9 版本之前定义内建类型的代码是这样写的:

type byte uint8
type rune int32

而在 Go 1.9 版本之后变为:

type byte = uint8
type rune = int32

这个修改就是配合类型别名而进行的修改。

类型定义:type Weekday int

类型别名:type newtype = oldtype

类型别名与类型定义区别:

  • 表面上看:类型别名多一个等号

  • 编译过程:类型的别名只会在代码中存在,编译完成时并不会有别名那个类型。

  • 代码示例如下:

    //类型定义
    type NewInt int
    
    //类型别名
    type MyInt = int
    
    func main() {
    	var a NewInt
    	var b MyInt
    
    	fmt.Printf("type of a:%T\n", a) //类型定义:type of a:main.NewInt
    	fmt.Printf("type of b:%T\n", b) //类型别名:type of b:int
    }
    

Go关键字与标识符

关键字即是被Go语言赋予了特殊含义的单词,也可以称为保留字。Go语言中的关键字一共有 25 个:

breakdefaultfuncinterfaceselect
casedefergomapstruct
chanelsegotopackageswitch
constfallthroughifrangetype
continueforimportreturnvar

标识符

标识符是指Go语言对各种变量、方法、函数等命名时使用的字符序列,标识符由若干个字母、下划线_、和数字组成,且第一个字符必须是字母。通俗的讲就是凡可以自己定义的名称都可以叫做标识符。

下划线_是一个特殊的标识符,称为空白标识符,它可以像其他标识符那样用于变量的声明或赋值(任何类型都可以赋值给它),但任何赋给这个标识符的值都将被抛弃,因此这些值不能在后续的代码中使用,也不可以使用_作为变量对其它变量进行赋值或运算。

标识符的命名需要遵守以下规则:

  • 由 26 个英文字母、0~9、_组成;
  • 不能以数字开头,例如 var 1num int 是错误的;
  • Go语言中严格区分大小写;
  • 标识符不能包含空格;
  • 不能以系统保留关键字作为标识符,比如 break,if 等等。

命名标识符时还需要注意以下几点:

  • 标识符的命名要尽量采取简短且有意义;
  • 不能和标准库中的包名重复;
  • 为变量、函数、常量命名时采用驼峰命名法,例如 stuName、getVal;

当然Go语言中的变量、函数、常量名称的首字母也可以大写,如果首字母大写,则表示它可以被其它的包访问;如果首字母小写,则表示它只能在本包中使用

在Go语言中还存在着一些特殊的标识符,叫做预定义标识符,如下表所示:

appendboolbytecapclosecomplexcomplex64complex128uint16
copyfalsefloat32float64imagintint8int16uint32
int32int64iotalenmakenewnilpanicuint64
printprintlnrealrecoverstringtrueuintuint8uintptr

预定义标识符一共有 36 个,主要包含Go语言中的基础数据类型和内置函数,这些预定义标识符也不可以当做标识符来使用。

字符串和数值类型的相互转换

Go语言中的 strconv 包提供了字符串和基本数据类型之间的转换功能,strconv 包中常用的函数包括 Atoi()、Itia()、parse 系列函数、format 系列函数、append 系列函数等

整型转字符串:Itoa()

func Itoa(i int) string  //函数签名
num := 100
str := strconv.Itoa(num)

字符串转整型:Atoi()

func Atoi(s string) (i int, err error)  
//函数签名可以看出 Atoi() 函数有两个返回值,i 为转换成功的整型,err 在转换成功是为空转换失败时为相应的错误信息
str1 := "110"
num1, err := strconv.Atoi(str1)

Parse系列函数:将字符串转换为指定类型的值,Parse 系列函数都有两个返回值,第一个返回值是转换后的值,第二个返回值为转化失败的错误信息。

ParseBool():字符串转换为bool类型的值

func ParseBool(str string) (value bool, err error)
//它只能接受 1、0、t、f、T、F、true、false、True、False、TRUE、FALSE,其它的值均返回错误
str1 := "1"
boo1, err := strconv.ParseBool(str1)

ParseInt():字符串转换为整数

func ParseInt(s string, base int, bitSize int) (i int64, err error)
str := "-11"
num, err := strconv.ParseInt(str, 10, 0)

ParseUint(): 函数的功能类似于 ParseInt() 函数,但 ParseUint() 函数不接受正负号,用于无符号整型

func ParseUint(s string, base int, bitSize int) (n uint64, err error)
str := "11"
num, err := strconv.ParseUint(str, 10, 0)

ParseFloat(): 函数用于将一个表示浮点数的字符串转换为 float 类型

func ParseFloat(s string, bitSize int) (f float64, err error)
str := "11"
num, err := strconv.ParseUint(str, 10, 0)

Format 系列函数

Format 系列函数实现了将给定类型数据格式化为字符串类型的功能,其中包括 FormatBool()、FormatInt()、FormatUint()、FormatFloat()

num := true
str := strconv.FormatBool(num)

var num int64 = 100
str := strconv.FormatInt(num, 16)

var num uint64 = 110
str := strconv.FormatUint(num, 16)

var num float64 = 3.1415926
str := strconv.FormatFloat(num, 'E', -1, 64)

Append 系列函数

Append 系列函数用于将指定类型转换成字符串后追加到一个切片中,其中包含 AppendBool()、AppendFloat()、AppendInt()、AppendUint()。

b16 := []byte("int (base 16):")
b16 = strconv.AppendInt(b16, -42, 16)  //将转换为10进制的string,追加到slice中

Go流程控制

if语句

if condition {
    // do something
}
/*
关键字 if 和 else 之后的左大括号{必须和关键字在同一行,如果你使用了 else if 结构,则前段代码块的右大括号}必须和 else if 关键字在同一行,这两条规则都是被编译器强制规定的。
*/
if condition1 {
    // do something
} else if condition2 {
    // do something else
}else {
    // catch-all or default
}

//以下是非法的
if x{
}
else { // 无效的, else要和上一个右大括号对齐
}

if 还有一种特殊的写法,可以在 if 表达式之前添加一个执行语句,再根据变量值进行判断,代码如下:

if err := Connect(); err != nil {
    fmt.Println(err)
    return
}
/*
这种写法可以将返回值与判断放在一行进行处理,而且返回值的作用范围被限制在 if、else 语句组合中。
*/

循坏结构

go只支持for关键字,不支持while和do while,for循坏结构和C类似,条件语句不用加括号

sum := 0
for i := 0; i < 10; i++ {
    sum += i
}

for死循坏:for;; {}简写成for {}

sum := 0
for {
    sum++
    if sum > 100 {
        break
    }
}

Go语言中的 for 循环与C语言一样,都允许在循环条件中定义和初始化变量,唯一的区别是,Go语言不支持以逗号为间隔的多个赋值语句,必须使用平行赋值的方式来初始化多个变量

break

Go语言的 for 循环同样支持 continue 和 break 来控制循环,但是它提供了一个更高级的 break,可以选择中断哪一个循环,如下例:break跳出的是JLoop标签标记的外层循坏,continue语句也支持标签功能

func main() {
	JLoop:
	for j := 0; j < 5; j++ {
		for i := 0; i < 10; i++ {
			if i > 5 {
				break JLoop  //跳出外层循坏
			}
			fmt.Println(i)
		}
	}
}
输出:0 1 2 3 4 5
package main
import "fmt"
func main() {
OuterLoop:
    for i := 0; i < 2; i++ {
        for j := 0; j < 5; j++ {
            switch j {
            case 2:
                fmt.Println(i, j)
                continue OuterLoop  //结束当前循环,开启下一次的外层循环
            }
        }
    }
}

仿while

i := 0
for ; i <= 10; {
	i++
}

简化为:(就像while)

i := 10
for i <= 10 {
    i++
}

for range键值循坏

for range 可以遍历数组、切片、字符串、map 及通道(channel),for range 语法上类似于其它语言中的 foreach 语句,一般形式为:(val 始终为集合中对应索引的值拷贝,因此它一般只具有只读性质,对它所做的任何修改都不会影响到集合中原有的值)

for key, val := range map1 {  
    //
}

通过 for range 遍历的返回值有一定的规律:

  • 数组、切片、字符串返回索引和值。
  • map 返回键和值。
  • 通道(channel)只返回通道内的值。

示例:

for key, value := range []int{1, 2, 3, 4} {
    fmt.Printf("key:%d, value:%d\n", key, value)
}

for key, value := range map1 {
    fmt.Println(key, value)
}

//只获取value值
for _, value := range m {
    fmt.Println(value)
}

switch

Go语言的 switch 要比C语言的更加通用,表达式不需要为常量,甚至不需要为整数

Go语言改进了 switch 的语法设计,case 与 case 之间是独立的代码块,不需要通过 break 语句跳出当前 case 代码块以避免执行到下一行,示例代码如下:

var a = "hello"
switch a {
    case "hello":
    fmt.Print(1)
    case "world":
    fmt.Print(2)
    default:
    fmt.Print(3)
}
输出:1

一分支多值

当出现多个 case 要放在一起的时候,可以写成下面这样:

var a = "mum"
switch a {
case "mum", "daddy":
    fmt.Println("family")
}

case 后不仅仅只是常量,还可以和 if 一样添加表达式,代码如下:

var r int = 11
switch {
case r > 10 && r < 20:
    fmt.Println(r)
}

fallthrough—兼容C的case

在Go语言中 case 是一个独立的代码块,执行完毕后不会像C语言那样紧接着执行下一个 case,但是为了兼容一些移植代码,依然加入了 fallthrough 关键字来实现这一功能,代码如下:(fallthrough只是为了兼容C,新编写的代码,不建议使用 fallthrough)

var s = "hello"
switch {
case s == "hello":
    fmt.Println("hello")
    fallthrough
case s != "world":
    fmt.Println("world")
}

输出:
hello
world

goto

Go语言中 goto 语句通过标签进行代码间的无条件跳转,同时 goto 语句在快速跳出循环、避免重复退出上也有一定的帮助,使用 goto 语句能简化一些代码的实现过程。

package main
import "fmt"
func main() {
    for x := 0; x < 10; x++ {
        for y := 0; y < 10; y++ {
            if y == 2 {
                // 跳转到标签
                goto breakHere
            }
        }
    }
    // 手动返回, 避免执行进入标签
    return
    // 标签
breakHere:
    fmt.Println("done")
}

使用goto可以集中处理错误,比如出现了错误,goto到一个处理错误的地方集中处理,如下代码示例:

err := firstCheckError()
if err != nil {
    goto onExit
}
err = secondCheckError()
if err != nil {
    goto onExit
}
fmt.Println("done")
return
onExit:
fmt.Println(err)
exitProcess()

练习:二分查找、冒泡排序

//使用go语言实现二分查找
package main

import "fmt"

func Binary(arr []int, left int, right int, target int) int {
	for left <= right {
		mid := left + (right-left)/2
		if arr[mid] == target {
			return mid
		} else if arr[mid] > target {
			right = mid - 1
		} else {
			left = mid + 1
		}
	}
	return -1
}

func main() {
	arr := []int{0, 1, 2, 3, 7, 8, 9, 10}
	fmt.Println(Binary(arr, 0, len(arr)-1, 7))
}
//go语言实现冒泡排序
package main

import "fmt"

func BubbleSort(arr *[]int) {
	end := len(*arr)
	for i := 0; i < end-1; i++ {
		for j := 0; j < end-i-1; j++ {
			if (*arr)[j] > (*arr)[j+1] {
				tmp := (*arr)[j]
				(*arr)[j] = (*arr)[j+1]
				(*arr)[j+1] = tmp
			}
		}
	}
}

func main() {
	arr := []int{1, 11, 9, 0, -1, 3}
	BubbleSort(&arr)
	for _, v := range arr {
		fmt.Println(v)
	}
}

Go语言函数

func 函数名(形式参数列表)(返回值列表){
    函数体
}

因为Go语言是编译型语言,所以函数编写的顺序是无关紧要的,鉴于可读性的需求,最好把 main() 函数写在文件的前面,其他函数按照一定逻辑顺序进行编写(例如函数被调用的顺序)

函数有助于代码重用(事实上,好的程序是非常注意 DRY 原则的,即不要重复你自己(Don’t Repeat Yourself),意思是执行特定任务的代码只能在程序里面出现一次)

return 语句可以带有零个或多个参数,这些参数将作为返回值供调用者使用,简单的 return 语句也可以用来结束 for 的死循环,或者结束一个协程(goroutine)

在函数调用时,Go语言没有默认参数值,也没有任何方法可以通过参数名指定形参

Go语言里面拥三种类型的函数:

  • 普通的带有名字的函数
  • 匿名函数或者 lambda 函数
  • 方法

如果一组形参或返回值有相同的类型,不必为每个形参都写出参数类型,下面 2 个声明是等价的:

func f(i, j, k int, s, t string) { /* ... */ }
func f(i int, j int, k int, s string, t string) { /* ... */ }

空白标识符_可以强调某个参数未被使用:

func first(x int, _ int) int { return x }

传值、传引用

Go语言中,string、int、bool、数组、stuct都属于非引用数据类型。

Go语言中,指针、Slice切片、map、chan都是引用数据类型,引用的时候也是类似指针地址

在函数中,实参通过值传递的方式进行传递,因此函数的形参是实参的拷贝,对形参进行修改不会影响实参,但是,如果实参包括引用类型,如指针、slice(切片)、map、function、channel 等类型,实参可能会由于函数的间接引用被修改

func fun(map1 map[string]int) {
	map1["one"] = 2
}

func main() {
	var map1 map[string]int = map[string]int{"one": 1}

	fmt.Println(map1["one"])

	fun(map1)

	fmt.Println(map1["one"])
}

输出:
1
2

返回值

Go语言支持多返回值,多返回值能方便地获得函数执行后的多个返回参数,Go语言经常使用多返回值中的最后一个返回参数返回函数执行中可能发生的错误,示例代码如下:

conn, err := connectToNetwork()  // connectToNetwork 返回两个参数,conn 表示连接对象,err 返回错误信息

多返回值示例:

func typedTwoValues() (int, int) {
    return 1, 2
}
func main() {
    a, b := typedTwoValues()
    fmt.Println(a, b)
}

带有变量名的返回值

Go语言支持对返回值进行命名,这样返回值就和参数一样拥有参数变量名和类型。

命名的返回值变量的默认值为类型的默认值,即数值为 0,字符串为空字符串,布尔为 false、指针为 nil 等。

下面代码中的函数拥有两个整型返回值,函数声明时将返回值命名为 a 和 b,因此可以在函数体中直接对函数返回值进行赋值,在命名的返回值方式的函数体中,在函数结束前需要显式地使用 return 语句进行返回,代码如下:

func namedRetValues() (a, b int) {  //对两个整型返回值进行命名a和b
    a = 1  //对返回值进行赋值
    b = 2
    return  //当函数使用命名返回值时,可以在 return 中不填写返回值列表,如果填写也是可行的
}

注意:同一种类型返回值和命名返回值两种形式只能二选一,混用时将会发生编译错误,例如下面的代码:

func namedRetValues() (a, b int, int)  //error:mixed named and unnamed function parameters

函数变量

在Go语言中,函数也是一种类型,可以和其他类型一样保存在变量中,下面的代码定义了一个函数变量 f,并将一个函数名为 fire() 的函数赋给函数变量 f,这样调用函数变量 f 时,实际调用的就是 fire() 函数,代码如下:

func fire() {
    fmt.Println("fire")
}

func main() {
    var f func()  //定义函数变量
    f = fire
    f()
}

匿名函数

匿名函数是指不需要定义函数名的一种函数实现方式,由一个不带函数名的函数声明和函数体组成

func(参数列表)(返回参数列表){  //匿名函数的定义就是没有名字的普通函数定义
    函数体
}

//在定义时调用匿名函数
func(data int) {
    fmt.Println("hello", data)
}(100)   //}后的(100),表示对匿名函数进行调用,传递参数为 100。

将匿名函数赋值给变量

// 将匿名函数体保存到f()中
f := func(data int) {
    fmt.Println("hello", data)
}
// 使用f()调用
f(100)

匿名函数用作回调函数:(类似C++里面lambda表达式的使用)

package main

import "fmt"

func visit(list []int, f func(int)) {  //第二个参数是一个函数对象/变量
	for _, v := range list {
		f(v)
	}
}

func main() {
	visit([]int{1, 2, 3, 4}, func(v int) {
		fmt.Println(v)
	})
}

闭包

闭包(Closure)在某些编程语言中也被称为 Lambda 表达式

Go语言中闭包是引用了自由变量的函数,被引用的自由变量和函数一同存在,即使已经离开了自由变量的环境也不会被释放或者删除,在闭包中可以继续使用这个自由变量,因此,简单的说:

函数 + 引用环境 = 闭包

闭包的结构很简单,一个是函数指针,另一个是对外部环境的引用

同一个函数与不同引用环境组合,可以形成不同的实例,如下图所示:

在这里插入图片描述

一个函数类型就像结构体一样,可以被实例化,函数本身不存储任何信息,只有与引用环境结合后形成的闭包才具有“记忆性”,函数是编译期静态的概念,而闭包是运行期动态的概念

str := "hello world"  // 准备一个字符串

// 创建一个匿名函数
foo := func() {
    str = "hello dude"  // 匿名函数中访问str, 在匿名函数中并没有定义 str,str 的定义在匿名函数之前,此时,str 就被引用到了匿名函数中形成了闭包
}
// 执行闭包,此时 str 发生修改,变为 hello dude。
foo()

可变参数

如同C中printf,o语言标准库中的 fmt.Println() 等函数的实现也依赖于语言的可变参数功能

可变参数是指函数传入的参数个数是可变的,为了做到这点,首先需要将函数定义为可以接受可变参数的类型:

func myfunc(args ...int) {  //函数 myfunc() 接受不定数量的参数,这些参数的类型全部是 int
    for _, arg := range args {
        fmt.Println(arg)
    }
}

func main() {
    myfunc(1, 2, 3, 4, 5)  //或者myfunc(1,2)参数可变
}

形如...type格式的类型只能作为函数的参数类型存在,并且必须是最后一个参数,它是一个语法糖(syntactic sugar),即这种语法对语言的功能并没有影响,但是更方便程序员使用,通常来说,使用语法糖能够增加程序的可读性,从而减少程序出错的可能。

从内部实现机理上来说,类型...type本质上是一个数组切片,也就是[]type,这也是为什么上面的参数 args 可以用 for 循环来获得每个传入的参数。

之前的例子中将可变参数类型约束为 int,如果你希望传任意类型,可以指定类型为 interface{},下面是Go语言标准库中 fmt.Printf() 的函数原型:

func Printf(format string, args ...interface{}) {  
    // ...
}
//用 interface{} 传递任意类型数据是Go语言的惯例用法,使用 interface{} 仍然是类型安全的

在多个可变参数中传递参数

可变参数变量是一个包含所有参数的切片,如果要将这个含有可变参数的变量传递给下一个可变参数函数,可以在传递时给可变参数变量后面添加...,这样就可以将切片中的元素进行传递,而不是传递可变参数变量本身。

// 实际打印的函数
func rawPrint(rawList ...interface{}) {
    // 遍历可变参数切片
    for _, a := range rawList {
        // 打印参数
        fmt.Println(a)
    }
}
// 打印函数封装
func print(slist ...interface{}) {
    // 将slist可变参数切片完整传递给下一个函数
    rawPrint(slist...)  //----------------------------------------将切片中的元素进行传递
}
func main() {
    print(1, 2, 3)
}

defer延迟执行

Go语言的 defer 语句会将其后面跟随的语句进行延迟处理,在 defer 归属的函数即将返回时,将延迟处理的语句按 defer 的逆序进行执行,也就是说,先被 defer 的语句最后被执行,最后被 defer 的语句,最先被执行。

defer一般用于释放某些已分配的资源,典型的例子就是对一个互斥解锁,或者关闭一个文件。

  • 代码的延迟顺序与最终的执行顺序是反向的。
  • 延迟调用是在 defer 所在函数结束时进行,函数结束可以是正常返回时,也可以是发生宕机时。
package main

import "fmt"

func main() {
	fmt.Println("defer begin")

	//开启延迟调用栈
	defer fmt.Println(1)
	defer fmt.Println(2)
	defer fmt.Println(3) //最后一个放入, 位于栈顶, 最先调用

	fmt.Println("defer end")
}

输出:
defer begin
defer end
3
2
1

使用延迟执行语句在函数退出时释放资源

处理业务或逻辑中涉及成对的操作是一件比较烦琐的事情,比如打开和关闭文件、接收请求和回复请求、加锁和解锁等。在这些操作中,最容易忽略的就是在每个函数退出处正确地释放和关闭资源。

defer 语句正好是在函数退出时执行的语句,所以使用 defer 能非常方便地处理资源释放问题。

  • 使用defer来延迟解锁
  • 使用defer来延迟释放文件句柄

宕机panic

Go语言的类型系统会在编译时捕获很多错误,但有些错误只能在运行时检查,如数组访问越界、空指针引用等,这些运行时错误会引起宕机。

一般而言,当宕机发生时:

  • 程序会中断运行,并立即执行在该 goroutine(可以先理解成线程)中被延迟的函数(defer 机制)
  • 随后,程序崩溃并输出日志信息,日志信息包括 panic value 和函数调用的堆栈跟踪信息,panic value 通常是某种错误信息。

手动触发宕机

Go语言可以在程序中手动触发宕机,让程序崩溃,这样开发者可以根据宕机时输出到控制台的堆栈和 goroutine 信息及时地发现错误,同时减少可能的损失。

package mainfunc main() {    panic("crash")}

painc()可以造成程序崩溃,panic函数签名如下:

func panic(v interface{})    //panic() 的参数可以是任意类型的。

注意:当 panic() 触发的宕机发生时,panic() 后面的代码将不会被运行,但是在 panic() 函数前面已经运行过的 defer 语句依然会在宕机发生时发生作用,

宕机恢复recover

Recover 是一个Go语言的内建函数,可以让进入宕机流程中的 goroutine 恢复过来,recover 仅在延迟函数 defer 中有效,在正常的执行过程中,调用 recover 会返回 nil 并且没有其他任何效果,如果当前的 goroutine 陷入panic,调用 recover 可以捕获到 panic 的输入值,并且恢复正常的执行。

panic 和 recover 的组合有如下特性:

  • 有 panic 没 recover,程序宕机。
  • 有 panic 也有 recover,程序不会宕机,执行完对应的 defer 后,从宕机点退出当前函数后继续执行。

注意:

虽然 panic/recover 能模拟其他语言的异常机制,但并不建议在编写普通函数时也经常性使用这种特性。

在 panic 触发的 defer 函数内,可以继续调用 panic,进一步将错误外抛,直到程序整体崩溃。

如果想在捕获错误时设置当前函数的返回值,可以对返回值使用命名返回值方式直接进行设置。

函数运行时间

func Since(t Time) Duration
package main

import (
	"fmt"
	"time"
)

func test() {
	start := time.Now() //获取当前时间----------------
	sum := 0
	for i := 0; i < 100000000; i++ {
		sum++
	}
	elapsed := time.Since(start)  //---------------------
	fmt.Println("该函数执行完成耗时:", elapsed)
}

func main() {
	test()
}

Since() 函数返回从 t 到现在经过的时间,等价于time.Now().Sub(t)

package main
import (
    "fmt"
    "time"
)
func test() {
    start := time.Now() // --------------------获取当前时间
    sum := 0
    for i := 0; i < 100000000; i++ {
        sum++
    }
    elapsed := time.Now().Sub(start)  //-----------------now-start = duration
    fmt.Println("该函数执行完成耗时:", elapsed)
}
func main() {
    test()
}

Go语言函数的底层实现

Go语言函数使用的是 caller-save 的模式,即由调用者负责保存寄存器,所以在函数的头尾不会出现push ebp; mov esp ebp这样的代码,相反其是在主调函数调用被调函数的前后有一个保存现场和恢复现场的动作。

  • 函数的调用者负责环境准备,包括为参数和返回值开辟栈空间。
  • 寄存器的保存和恢复也由调用方负责。
  • 函数调用后回收栈空间,恢复 BP 也由主调函数负责。

主调函数保存和恢复现场的通用逻辑如下:

//开辟栈空间,压栈 BP 保存现场
    SUBQ $x, SP    //为函数开辟裁空间
    MOVQ BP, y(SP) //保存当前函数 BP 到 y(SP)位直, y 为相对 SP 的偏移量
    LEAQ y(SP), BP //重直 BP,使其指向刚刚保存 BP 旧值的位置,这里主要
                   //是方便后续 BP 的恢复
//弹出栈,恢复 BP
    MOVQ y(SP), BP //恢复 BP 的值为调用前的值
    ADDQ $x, SP    //恢复 SP 的值为函数开始时的位

函数的多值返回实质上是在栈上开辟多个地址分别存放返回值,这个并没有什么特别的地方,如果返回值是存放到堆上的,则多了一个复制的动作。

函数调用前己经为返回值和参数分配了栈空间,分配顺序是从右向左的,先是返回值,然后是参数,通用的栈模型如下:

----------| 返回值 y   |
|-----------|
| 返回值 x   |
|-----------|
|  参数 b    |
|-----------|
|  参数 a    |----------

函数的多返回值是主调函数预先分配好空间来存放返回值,被调函数执行时将返回值复制到该返回位置来实现的。

Test功能测试函数

Go语言自带了 testing 测试包,可以进行自动化的单元测试,输出结果验证,并且可以测试性能。

测试规则

要开始一个单元测试,需要准备一个 go 源码文件,在命名文件时文件名必须以_test.go结尾,单元测试源码文件可以由多个测试用例(可以理解为函数)组成,每个测试用例的名称需要以 Test 为前缀,例如:

func TestXxx( t *testing.T ){
    //......
}

编写测试用例有以下几点需要注意:

  • 测试用例文件不会参与正常源码的编译,不会被包含到可执行文件中;

  • 测试用例的文件名必须以_test.go结尾;

  • 需要使用 import 导入 testing 包;

  • 测试函数的名称要以TestBenchmark开头,后面可以跟任意字母组成的字符串,但第一个字母必须大写,例如 TestAbc(),一个测试用例文件中可以包含多个测试函数;

  • 单元测试则以(t *testing.T)作为参数,性能测试以(t *testing.B)做为参数;

  • 测试用例文件使用go test命令来执行,源码中不需要 main() 函数作为入口,所有以_test.go结尾的源码文件内以Test开头的函数都会自动执行。

Go语言的 testing 包提供了三种测试方式,分别是单元(功能)测试、性能(压力)测试和覆盖率测试。

单元(功能)测试

在同一文件夹下创建两个Go语言文件,分别命名为 demo.go 和 demt_test.go,如下图所示:

在这里插入图片描述

性能(压力)测试

将 demo_test.go 的代码改造成如右边所示的样子:

在这里插入图片描述

覆盖率测试

覆盖率测试能知道测试程序总共覆盖了多少业务代码(也就是 demo_test.go 中测试了多少 demo.go 中的代码),可以的话最好是覆盖100%。

将 demo_test.go 代码改造成如右边所示的样子:

在这里插入图片描述

附录:

Go标准库

Go语言的标准库以包的方式提供支持,下表列出了Go语言标准库中常见的包及其功能。

Go语言标准库包名功 能
bufio带缓冲的 I/O 操作
bytes实现字节操作
container封装堆、列表和环形列表等容器
crypto加密算法
database数据库驱动和接口
debug各种调试文件格式访问及调试功能
encoding常见算法如 JSON、XML、Base64 等
flag命令行解析
fmt格式化操作
goGo语言的词法、语法树、类型等。可通过这个包进行代码信息提取和修改
htmlHTML 转义及模板系统
image常见图形格式的访问及生成
io实现 I/O 原始访问接口及访问封装
math数学库
net网络库,支持 Socket、HTTP、邮件、RPC、SMTP 等
os操作系统平台不依赖平台操作封装
path兼容各操作系统的路径操作实用函数
pluginGo 1.7 加入的插件系统。支持将代码编译为插件,按需加载
reflect语言反射支持。可以动态获得代码中的类型信息,获取和修改变量的值
regexp正则表达式封装
runtime运行时接口
sort排序接口
strings字符串转换、解析及实用函数
time时间接口
text文本模板及 Token 词法器

使用代理

VSCode插件、go官网有的东西安装失败,使用代理

  • go env -w GO111MODULE=on
  • go env -w GOPROXY=https://goproxy.io,direct

参考文献

Go语言圣经 | 中文版
Go语言入门教程
Go语言中文社区

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值