一 变量
1.1 变量的声明
1.1.1 标准格式
var 变量名 变量类型
var a int // 声明一个整形类型变量
var b string // 声明一个字符串类型变量
var c []float32 // 声明一个32位的浮点类型的变量
var d func() bool // 声明一个函数变量,变量值是bool,将函数以变量形式保存
// 声明一个结构体的变量
var e struct{
x int
}
1.1.2 批量格式
var (
变量名 变量类型
)
var ( x int y string z []float32 )
1.2 初始化变量
我们可以在声明变量的时候对变量进行赋值,如果不赋值,也会进行初始化。
// go语言在声明变量的时候,自动对变量对应的内存区域进行初始化操作,每一个变量会初始化其类型的默认值 // 整形和浮点型默认值是0 // 字符串默认值是空串 // 布尔类型默认为bool // 切片、函数和指针变脸默认为nil
注意:我们在使用C语言的时候,如果声明了变量,没有初始化,可能该变量指向的内存区域的值没有被清理,容易产生问题。所以开发者在C中需要习惯对变量进行初始化。
1.2.1 标准初始化格式
var 变量名 类型 = 变量值
var a int = 100 var b string = "hello"
1.2.2 编译器推导类型
// 编译器推导类型(省略数据类型,由编译器自己确定) var x = 122.5 var y = a * 2
1.2.3 短变量声明并初始化
还有一种更为精简的写法,var关键字都不需要
p := 200
由于使用:= 所以左边的变量必须是没有定义过的,如果是这样的就不允许:
var p int p := 200
编译就会报错
使用方法:
import "net"
func main() { conn,err := net.Dial("tcp","127.0.0.1") }
如果是多个短变量,至少有一个是新声明的变量,其他都是已经声明过得,也不会报错。
1.2.4 多个变量同时赋值
以前的时候交换值如下:
var m int = 100 var n int = 20 var t int t = m m = n n = t
后面有了更精简的交换:
func main() { var m int = 100 var n int = 20 m = m ^ n n = n ^ m m = m ^ n fmt.Println(m,n) }
Go语言,现在写法更加简单,可以进行多重复值
格式:m,n=n,m
多重赋值的时候,是变量先从左边开始赋值,然后到右边,这里就是先把n的值赋给m,再把m的值赋给n
func main() { var m int = 100 var n int = 20 m,n = n,m fmt.Println(m,n) }
1.2.5 匿名变量(没有名字的变量)
就是使用下划线,表示匿名变量,比如在多重赋值的时候,如果左边值中某一变量不需要接收变量,则可以使用_匿名变量替换即可
func main() { c,_ := getData() _,d := getData() fmt.Println(c,d) }
func getData()(int,int) { return 200,500; }
二 数据类型以及类型转换
2.1 基本数据类型
2.1.1 整形
int 主要分为8位、16位、32位、64位,即int8/int16/int32/int64,也就是1字节、2字节、4字节、8字节
还有对应的无符号整形:uint8/unit16/uint32/uint64
无符号和有符号区别:即最高位是否可以是否表示符号,比如1字节,最大值就是2^8=256,最小就是1,用无符号uint8表示就是[0,255]; 有符号就是[-128,127] 个数也是256个。
2.1.2 浮点型
Go语言支持2种浮点类型:float32和float64,可以使用常亮max.MaxFloat32和max.MaxFloat64,如果使用格式化参数替换的时候,要是用%f
import "fmt"
func main() {
var a float32 = 100.3456
var b float64 = 10.2
fmt.Printf("%f\n",a)
fmt.Printf("%.2f\n",a)
fmt.Printf("%.2f\n",b)
}
100.345596
100.35
10.20
2.1.3 布尔型
bool变量只有true和false两种值,午饭参与运算,也无法进行类型转换
2.1.4 字符串
用双引号括起来的就是字符串,比如:
var c string="hello"
var d = "cool"
e := "honey"
fmt.Print(c,d,e)
转义字符:
\r: 回车符 \n:换行符 \t: 制表符 \'单引号 \"双引号 \\ 反斜杠/
定义多行字符串:通过反引号,将原始内容全部赋值给变量,不进行转义
var multi_line string = `
hello word
cool boy
\r\n\t
`
fmt.Print(multi_line)
hello word
cool boy
\r\n\t
2.1.5 字符
Go语言中有两种表示字符的数据类型:byte和rune
byte: 是一个int8类型,表示单个拉丁字母
rune: 是一个int32类型,表示 UTF-8字符,比如中文、日文等
var x byte = 'a'
var y rune = '好'
fmt.Printf("%d %T\n",x,x)
fmt.Printf("%d %T\n",y,y)
97 uint8
22909 int32
UTF-8和Unicode有何区别?
Unicode是字符集,ASCII也是字符集。
字符集为每一个字符分配一个唯一的ID,我们使用到的字符在Unicode中都有唯一的ID和她对应,比如你在Unicode中编码为20320,但是在不同的国家中,你的ID会不同,但是Unicode中的字符ID是不会变化
UTF-8是编码规则:将Unicode中字符ID以某种方式编码,它是一种变长编码规则,从1到4字节不等,编码规则如下:
#1 0xxxxx表示符号0-127,兼容ASCII字符集
#2 从128 到0x10ffff表示其他字符
2.1.6 切片
切片,类似于数组,表示相同类型元素的容器,和数组不同的是,数组长度是不可变的,切片是可变长的,声明方式如下:
var arr []int
// 创建长度为3切片,没有值
arr := make([]int,3)
// 为切片元素赋值
arr[0] = 1
arr[1] = 2
arr[2] = 3
// 声明字符串
str := "hello world"
// 根据字符串从指定位置开始进行切片
fmt.Printf(str[6:])
2.2 类型转化
Go语言使用类型前置括号的方式进行类型转换,一般格式如下:
T(表达式)
其中T表示需要转换的类型,表达式包括变量,复杂算子和函数返回值等
fmt.Println("int8 range: ", math.MaxInt8,math.MaxInt8)
fmt.Println("int16 range: ", math.MaxInt16,math.MaxInt16)
fmt.Println("int31 range: ", math.MaxInt32,math.MaxInt32)
fmt.Println("int64 range: ", math.MaxInt64,math.MaxInt64)
// 初始化一个32位整数值
var m int32 = 1047483647
// 输出16进制和10进制
fmt.Println("int32: 0x%x %d\n", m,m)
// m转化为16进制
n := int16(m)
fmt.Println("int16: 0x%x %d\n", n,n)
// 将常亮保存为float32
var p float32 = math.Pi
// 转化为int类型,浮点发生精度丢失
fmt.Println(int(p))
三 指针
Go语言的指针区别于C、C++,不能进行便宜和运算,是一种安全的指针。要搞明白Go语言中的指针,需要搞懂以下几个概念:
指针类型:表示某变量是真真类型,用 *数据类型 表示指针类型,我们不能对指针类型赋其他值,比如int或者float之类的。创建指针类型有2种方式:
#1 通过*数据类型
#2 new(数据类型)
import "fmt"
func main() {
var foo int
var bar *int
// 获取变量值和变量类型
fmt.Printf("%v %T\n",foo,foo)
fmt.Printf("%v %T\n",bar,bar)
}
0 int
<nil> *int
func main() {
// 第二种创建指针的方式 new
str := new(string)
num := new(int16)
fmt.Println(str,num)
}
指针地址:变量在内存中的地址,我们可以通过 &变量名 获取该变量的地址,即指针指向的内存地址
import "fmt"
func main() {
var foo int
var bar *int
// 获取变量foo的指针地址
bar = &foo
// 获取变量值和变量类型
fmt.Printf("%v %T\n",foo,foo)
fmt.Printf("%v %T\n",bar,bar)
}
0 int
0xc00000a0a8 *int
指针取值:就是取该变量对应指针的值,通过 *变量名 就可以获取这个变量的指针的值。
import "fmt"
func main() {
var foo int = 10
var bar *int
// 获取变量foo的指针地址
bar = &foo
// 获取变量值和变量类型
fmt.Printf("%v %T\n",foo,foo)
// 通过*bar获取bar类型的值
fmt.Printf("%v %T\n",*bar,*bar)
}
10 int
10 int
我们可以对指针取值进行赋值,改变原来的值
import "fmt"
func main() {
var foo int = 10
var bar *int
// 获取变量foo的指针地址
bar = &foo
// 对获取foo变量指针地址重新赋值,这样就会修改foo的值
*bar = 100
// 获取变量值和变量类型
fmt.Printf("%v %T\n",foo,foo)
// 通过*bar获取bar类型的值
fmt.Printf("%v %T\n",*bar,*bar)
}
100 int
100 int
公式:*&x = x
综合测试:
func test() {
var a int = 1
var b *int = &a
// 因为是**int 表示直接把后面的值赋给变量,即var c = &b
var c **int = &b
var d int = *b
fmt.Println("a = ", a) // a = 1
fmt.Println("&a = ", &a) // 取a的内存地址(指针地址) 0xc000062080
fmt.Println("*&a = ",*&a) // 对&a表示的指针地址进行取值,也就是1
fmt.Println("b = ", b) // 因为&a赋给了b,所以b=&a即a的内存地址 0xc000062080
fmt.Println("&b = ", &b) // &b就是取b这个变量的地址 0xc00008e018
fmt.Println("*&b = ", *&b) // 对&b表示的指针地址取值,也就是b的值,即a的指针地址 0xc000062080
fmt.Println("*b = ", *b) // 对b表示的a内存地址取值,也就是1
fmt.Println("c = ", c) // 因为是**int 表示直接把后面的值赋给变量,即 c = &b,即b的地址 0xc00008e018
fmt.Println("*c = ", *c) // 对变量c去指针地址的值,就等于*&b,即取b指针地址的值,而b指针的值就是a的指针地址 0xc000062080
fmt.Println("&c = ", &c) // 取c的指针地址 0xc00008e020
fmt.Println("*&c = ", *&c) // 取c指针地址的值,即c的值,c值就是b的地址 0xc00008e018
fmt.Println("**c = ", **c) // 先取变量c指针地址,即a的指针地址,再对a的指针地址取值,即1
fmt.Println("***&*&*&*&c= ",***&*&*&*&c) // 因为*&c= c,推出这个可以简化为**c即1
fmt.Println("d = ", d) // 对b表示的a内存地址取值,也就是1
}
四 变量生命周期
变量的生命周期指的是变量使用范围。在C或者C++,程序员需要考虑变量内存分配问题,哪些变量需要使用栈进行内存分配,哪些使用堆内存分配。一般而言,函数体内的私有线程的变量一般使用栈进行分批;全局变量、结构体成员适合使用堆分配。Go语言将整个过程合并到编译器中,叫做变量逃逸分析,程序员对于内存分配过程无感知。
五 字符串应用
5.1 计算字符长度
ASCII字符串使用len计算长度;如果包含Unicode字段则使用utf8.RuneCountInString()。
len()函数可以用来获取切片、字符串、通道的长度。
foo := "love you,and love me"
bar := "四川省"
// 长度为19
fmt.Println("计算字符长度: ",len(foo))
// 长度为9, why? 难道不是3吗? go语言中字符串都是以UTF-8格式保存,每一个中文占用3个字节,所以三个中文字符就是9个字节长度
fmt.Println("计算字符长度: ",len(bar))
// 那如果按照中国习惯该如何使用呢,go提供了utf8.RuneCountInString函数
fmt.Println("计算字符长度: ", utf8.RuneCountInString(bar))
5.2 遍历字符串,获取每一个字符串元素
5.2.1 遍历每一个ASCII字符,直接使用下标
for i := 0; i < len(foo); i++{
fmt.Printf("ASCII: %c %d\n",foo[i],foo[i])
}
5.2.2 遍历带Unicode字符串的每一个字符,需要使用for range
for _,s:= range bar {
fmt.Printf("Unicode: %c %d\n",s,s)
}
5.3 获取指定字符串索引和截取字符串
5.3.1 strings.Index:获取某个字符的索引位置(正向搜索)
comma := strings.Index(foo,",")
fmt.Println("正向索引:",foo[comma:])
// 获取comma位置开始截取之后字符串,包括该位置的元素
fmt.Println("正向截取字符串:",foo[comma:])
// 截取指定开始位置和结束位置的元素
fmt.Println("正向截取字符串:",foo[comma+5:comma+9])
pos := strings.Index(foo[comma:],"love")
fmt.Println("正向截取字符串:",foo[comma+pos:])
5.3.2 strings.LastIndex:获取某个字符的索引位置(反向搜索)
idx := strings.LastIndex(foo,"me");
fmt.Println("反向索引",idx)
fmt.Println("反向截取字符串:",foo[idx:])
5.4 修改字符串
Go无法将字符串直接进行修改,只能通过重新构造新的字符串赋予给原来的字符串变量实现。
Go语言和字符串和其他高级语言,比如Java一样,都是Immutable不可变的,这样可以保证天生线程安全,大家使用的都是只读对象。无需加锁,而且方便共享内存。
怎么修改呢? 可以将字符串转化为byte数组,因为byte数组是可以变化的,本身是一个切片,然后转化完成再将byte数组转换成字符串。
angle := "heros never die"
angleBytes := []byte(angle)
for i:=5; i<=10;i++ {
angleBytes[i] = ' '
}
fmt.Println("修改字符串",string(angleBytes))
5.5 拼接字符串
Go语言可以使用+来连接字符串,但是和Java一样,此种方式效率不高,Go语言中也提供了StringBuilder的机制来进行高效的拼接
one := "我爱你"
two := ","
three := "成都"
// 声明字节缓冲区
var builder bytes.Buffer
// 调用WriteString,把字符串写入缓冲区
builder.WriteString(one)
builder.WriteString(two)
builder.WriteString(three)
// 调用String(),将缓冲以字符串形式输出
fmt.Println(builder.String())
5.6 格式化字符串
// %v: 按照值本来输出
// %+v:在%v基础上对结构体字段名和值进行展开
// %#v:输出Go语言语法格式的值
// %T:输出类型
// %b:二进制格式输出;%o: 八进制 %d:十进制 %x 十六进制 %X: 16进制,字母大写 %U:Unicode字符 %f: 浮点数 %p指针
// 按照指定格式
name := "我"
num := 999
word := fmt.Sprintf("你的酒馆对%s打了烊,但是还是送了你%d朵玫瑰",name,num)
fmt.Println("[字符串格式化]",word)
// 直接输出值
pi := 3.14159
variant := fmt.Sprintf("%v,%v,%v","思密达",pi,true)
fmt.Println("[字符串格式化]",variant)
profile := &struct {
Name string
HP int
}{
Name: "rat",
HP: 150,
}
fmt.Printf("使用'%%+v' %+v\n",profile)
fmt.Printf("使用'%%#v' %#v\n",profile)
fmt.Printf("使用'%%T' %T\n",profile)
[字符串格式化] 你的酒馆对我打了烊,但是还是送了你999朵玫瑰
[字符串格式化] 思密达,3.14159,true
使用'%+v' &{Name:rat HP:150}
使用'%#v' &struct { Name string; HP int }{Name:"rat", HP:150}
使用'%T' *struct { Name string; HP int }
5.7 去掉多余的空白字符
/** 7 去掉多余的空白字符 */
fmt.Println(strings.TrimSpace(keys));
5.8 对字符串切片,分成不同部分
/** 8 对字符串切片,分成不同部分 */
text := "中国:四川:成都";
splits := strings.Split(text,":");
for i := 0; i < len(splits);i++{
fmt.Println(splits[i])
}
六 常量和类型别名
6.1 常量
6.1.1 普通常量
func main() {
// 常量的声明把var换成了const
const pi = 3.14159
const e = 2.718281
const NUM int32 = 16
const TOKEN string = "token"
}
6.1.2 枚举
const (
SPRING = 0
SUMMER = 1
AUTUMN = 2
WINTER = 3
)
fmt.Println("[枚举类型]",SPRING,SUMMER,AUTUMN,WINTER)
6.2 类型别名
type 别名 = 类型(Type)
6.2.1 区分类型别名和类型定义
// 自定义类型,声明的变量是自定义类型
type NewInt int16
// 定义类型别名,声明的变量是还是原来的类型
type NewString = string
var a NewInt = 10
var b NewString = "NewString"
// 自定义类型,声明的变量是自定义类型,所以结果是NewInt
fmt.Printf("a type: %T\n",a)
// 定义类型别名,声明的变量是还是原来的类型,所以结果是string
fmt.Printf("b type: %T\n",b)
6.2.2 不能使用别名类型在非本地中定义方法
type MyDuration = time.Duration
// Cannot define new methods on non-local type time.Duration
func (m MyDuration) EasySet(a string){
}
非本地方法指的是在time.Duration 以外的其他包下,比如这里的main包,和time.Duration不在同一个包。那如果我一定要使用呢,有没有什么办法?
第一:我们可以将别名定义为自定义类型,即 type MyDuration time.Duration
type MyDuration time.Duration
func (m MyDuration) EasySet(a string){
}
第二:将这个别名类型定义在time包中