1. 格式化输出
对数值进行格式化输出的时候,使用:
%v 可用来表示复数;
%t 来表示输出的值为布尔型;
%d 来表示输出的值为整数;
%x和%X 来表示输出的值为16进制的数字;
%g 来格式化浮点型;
%f 用来输出浮点数;
%e 输出科学计数表示法;
%0d 用于规定输出定长的整数,其中开头的数字 0 是必须的;
%n.mg 用于表示数字 n 并精确到小数点后 m 位,除了使用 g 之外,还可以使用 e 或者 f,例如:使用格式化字符串 %5.2e 来输出 3.4 的结果为 3.40e+00。
2. int 和 int64 是相同的类型吗?
不是。
用代码去验证:
package main
func main(){
var a int
var b int64
a = 15
// b = a + a //编译错误: cannot use a + a (type int) as type int64 in assignment
b = a + 5 //5
}
总结一句话:
int 在32位系统上是4个字节
int 在64位系统上是8个字节
int32 在多少位系统都是4个字节
int64 在多少位系统都是8个字节
3. 算数优先级
优先级 运算符
7 ^ !
6 * / % << >> & &^
5 + - | ^
4 == != < <= >= >
3 <-
2 &&
1 ||
4. 类型更名
可通过使用type TZ int
,将TZ变成int类型的新名称,如
package main
import "fmt"
type TZ int
func main() {
var a, b TZ = 3, 4
c := a + b
fmt.Printf("c has the value: %d", c) // 输出:c has the value: 7
}
注意:类型别名得到的新类型并非和原类型完全相同,新类型不会拥有原类型所附带的方法
5. 字符的格式化输出
格式化说明符 %c 用于表示字符;
当和字符配合使用时,%v 或 %d 会输出用于表示该字符的整数;
%U 输出格式为 U+hhhh 的字符串。
6. go不支持函数重载
函数重载(function overloading)
指的是可以编写多个同名函数,只要它们拥有不同的形参 / 或者不同的返回值。
7. 值类型和引用类型
- 值类型:基本数据类型,如 int系列、float系列、bool、string、
数组
和结构体 struct
。 - 引用类型:指针、slice切片、map、管道chan、interface等。
区别:
- 值类型:变量直接存储值,内存通常在栈中分配
- 引用类型:变量存储的是一个地址,这个地址对应的空间才真正存储数据值,内存通常在堆上分配,当没有任何变量引用这个地址时,该地址对应的数据空间就成为一个垃圾,有GC来回收。
8. 函数命名规范
函数的命名规范遵循标识符命名规范,首字母不能是数字。
首字母大写该函数可以被本包文件和其它包文件使用。类似public,首字母小写,只能被本包文件使用,其它包文件不能使用,类似privat。
9. init函数的执行顺序
每一个源文件都可以包含一个init函数,init函数总是在main函数之前执行的。
如果一个文件同时包含全局变量定义、init 函数和main 函数,则执行的流程为:全局变量定义 —> init 函数 —> main 函数
package main
import (
"fmt"
)
var age = test() //全局变量
//为了可以看到全局变量是先被初始化的
func test() int {
fmt.Println("test()执行了")
return 23
}
//init函数,通常用来完成初始化操作
func init(){
fmt.Println("init()执行了")
}
func main(){
fmt.Println("main()执行了...age=", age)
}
10. 包之间的函数执行顺序
假设有两个源文件 main.go 和 utils.go ,它们分别在main目录和util目录下,main.go需要引用utils.go并使用其内部的函数。且这两个源文件的内部都有全局变量和init函数。
则执行的顺序是:utils的全局变量 —> utils的init函数 —> main的全局变量 —> main的init函数 —> main函数
11. 匿名函数
Go支持匿名函数,匿名函数就是没有名字的函数。如果某个函数只是使用一次的话,可以考虑使用匿名函数。当然,匿名函数也可以实现多次调用。
匿名函数的使用方式:
- 在定义匿名函数时就直接调用,这种方式匿名函数只能调用一次。
func main(){
res := func(n1 int, n2 int) int {
return n1 + n2
}(10, 20)
fmt.Println("res=", res)
}
- 将匿名函数赋给一个变量(函数变量),再通过该变量来调用匿名函数。
func main(){
a := func(n1 int, n2 int) int {
return n1 - n2
}
res1 := a(10, 30)
fmt.Println("res1=", res1)
res2 := a(50, 30)
fmt.Println("res2=", res2)
}
扩展:全局匿名函数
如果将匿名函数赋给一个全局变量,那么这个匿名函数就成为一个全局匿名函数。
package main
import (
"fmt"
)
//demo为一个全局匿名函数
var (
demo = func (n1 int, n2 int) int {
return n1 * n2
}
)
func main(){
res := demo(8, 9)
fmt.Println("res=", res)
}
12. 闭包
闭包就是一个函数和与其相关的引用环境组合的一个整体(实体)。
//AddUper()函数返回的是一个函数
func AddUper() func (int) int {
var n int = 10
return func(x int) int {
n = n + x
return n
}
}
func main(){
f := AddUper() // 这时f是一个函数
fmt.Println(f(1)) // 11
fmt.Println(f(2)) // 13
fmt.Println(f(3)) // 16
}
上述代码说明:
- AddUper() 是一个函数,其返回的数据类型是一个函数,即 func (int) int
- AddUper()返回的是一个匿名函数,但是这个匿名函数引用到函数外的n,所以这个匿名函数就和n形成一个整体,构成闭包。
- 当反复调用f函数时,因为n是初始化一次,所以每调用一次就进行累计。
再举一个例子:
func add(demo int) func(int) int {
fmt.Printf("%p\n", &demo) //打印变量地址,可以看出来 内部函数对外部传入参数的引用
f := func(i int) int {
fmt.Printf("%p\n", &demo)
demo += i
return demo
}
return f
}
func main(){
t := add(5)
fmt.Println(t(1), t(2)) //输出打印的值为:6 8
}
add()函数返回的是一个匿名函数,该匿名函数引用了外部传入的参数demo,从而匿名函数和demo构成了一个整体。
闭包的实现确保只要闭包还被使用,那么被闭包引用的变量会一直存在。
13. defer
defer其实是一种延时机制,功能上等同于Java的finally语句,会在函数执行完后,最后去执行。
func main(){
res := sum(20, 30)
fmt.Println("res=", res)
}
func sum(n1 int, n2 int) int {
//当函数执行到defer时,会暂不执行
defer fmt.Println("n1=", n1)
defer fmt.Println("n2=", n2)
res := n1 + n2
defer fmt.Println("ans res=", n1)
return res
}
注意:碰到defer的语句的时候,会将defer后面的语句压入到独立的栈中(像defer栈),然后在函数执行完后,再从defer栈中按照先入后出的方式执行。所以会先打印n2,再打印n1。
在上面的例子的基础上,再加入代码。
func main(){
res := sum(20, 30)
fmt.Println("res=", res)
}
func sum(n1 int, n2 int) int {
//当函数执行到defer时,会暂不执行
defer fmt.Println("n1=", n1)
defer fmt.Println("n2=", n2)
//新增代码
n1++
n2++
res := n1 + n2
defer fmt.Println("ans res=", n1)
return res
}
注意:这时也是先输出n2的值,再输出n1,但是n2=30,n1=20。因为函数是先执行了defer语句,已经压栈了,再值增加也不会产生影响。
14. 函数参数传递方式
有两种传递方式:
- 值传递
- 引用传递
值类型参数默认就是值传递,引用类型参数默认的是引用传递。
但是,不管是值传递还是引用传递,传递给函数的都是变量的副本,不同的是,值传递的是值的拷贝,引用传递的是地址的拷贝,一般来说,地址拷贝效率高,因为数据量小,而值拷贝是取决于拷贝的数据大小,数据越大,效率越低。
15. 切片创建方法对比
方法一:定义一个切片,然后让切片去引用一个已经创建好的数组。
func main(){
//创建一个数组
var arr [5]int = [...]int {1,2,3,4,5}
//创建一个切片
var sli = arr[1:3]
fmt.Println("arr=", arr)
fmt.Println("sli=", sli)
fmt.Println("sli len=", len(sli))
fmt.Println("sli=", cap(sli))
}
方法二:通过make的方式来创建切片
func main(){
//创建一个切片
var sli []float64 = make([]float64, 5, 10)
sli[1] = 20
sli[2] = 30
fmt.Println("sli=", sli)
fmt.Println("sli len=", len(sli))
fmt.Println("sli=", cap(sli))
}
方法一和方法二的区别:
方法一是直接引用数组,数组原先就存在,程序员是可见的。
方法二是通过make来创建的,make也会创建一个数组,但是由切片在底层进行维护,程序员是不可见的。
16. 切片(slice)
一个切片是一个数组片段的描述。它包含了指向数组的指针,片段的长度, 和容量(片段的最大长度)。
17. 切片的拷贝
func main(){
var a[]int = []int {1,2,3,4,5}
var sli = make([]int, 1)
fmt.Println(sli) // [0]
copy(sli, a)
fmt.Println(sli) // [1]
}
上述代码表示
当需要拷贝的切片(sli)长度小于被拷贝切片(a)的长度,都能够拷贝成功。
18. 切片的引用传递底层分析
代码1
func main(){
var slice []int
var arr [5]int =[...]int {1,2,3,4,5}
slice = arr[:]
var slice2 = slice
slice2[0] = 10
fmt.Println("slice2=", slice2) // [10,2,3,4,5]
fmt.Println("slice=", slice) // [10,2,3,4,5]
fmt.Println("arr=", arr) // [10,2,3,4,5]
}
代码2
func test(slice []int){
slice[0] = 100 // 修改了slice[0]的值,会改变实参
}
func main(){
var slice = []int {1,2,3,4,5}
fmt.Println("slice=", slice) // [1,2,3,4,5]
test(slice)
fmt.Println("slice=", slice) // [100,2,3,4,5]
}
两段代码可以看到切片是引用类型,在进行传递的时候,必须遵守引用传递机制(即传递的是地址)。
19. map的遍历
golang的map默认是无序
的,也不是按照添加的顺序存放的,所以每次遍历,得到的输出结果可能不一样。
使用方法:先将key进行排序,然后根据key值遍历输出。
不断在学习go中,这些为学习中个人觉得需要记录下来的知识点,后续会不断地更新。。。。