编码
ASCII表
众所周知,计算机起源于美国,英文只有26个字符,算上其他所有特殊符号也不会超过128个。字节是计算机的基本储存单位,一个字节(bytes)包括八个比特位(bit),能够表示出256个二进制数字,所以美国人在这里只是用到了一个字节的前七位即127个数字来对应了127个具体字符,而这张对应表就是ASCII码字符编码表,简称ASCII表。后来为了能够让计算机识别拉丁文,就将一个字节的最高位也应用了,这样就多扩展出128个二进制数字来对应新的符号。这张对应表因为是在ASCII表的基础上扩展的最高位,因此称为扩展ASCII表。到此位置,一个字节能表示的256个二进制数字都有了特殊的符号对应。
GBK编码
但是,当计算机发展到东亚国家后,问题又出现了,像中文,韩文,日文等符号也需要在计算机上显示。可是一个字节已经被西方国家占满了。于是,我中华民族自己重写一张对应表,直接生猛地将扩展的第八位对应拉丁文全部删掉,规定一个小于127的字符的意义与原来相同,即支持ASCII码表,但两个大于127的字符连在一起时,就表示一个汉字,这样就可以将几千个汉字对应一个个二进制数了。而这种编码方式就是GB2312,也称为中文扩展ASCII码表。再后来,我们为了对应更多的汉字规定只要第一个字节是大于127就固定表示这是一个汉字的开始,不管后面跟的是不是扩展字符集里的内容。这样能多出几万个二进制数字,就算甲骨文也能够用了。而这次扩展的编码方式称为GBK标准。当然,GBK标准下,一个像”苑”这样的中文符号,必须占两个字节才能存储显示。
Unicode与utf8编码
与此同时,其它国家也都开发出一套编码方式,即本国文字符号和二进制数字的对应表。而国家彼此间的编码方式是互不支持的,这会导致很多问题。于是ISO国际化标准组织为了统一编码,统计了世界上所有国家的字符,开发出了一张万国码字符表,用两个字节即六万多个二进制数字来对应。这就是Unicode编码方式。这样,每个国家都使用这套编码方式就再也不会有计算机的编码问题了。Unicode的编码特点是对于任意一个字符,都需要两个字节来存储。这对于美国人而言无异于吃上了世界的大锅饭,也就是说,如果用ASCII码表,明明一个字节就可以存储的字符现在为了兼容其他语言而需要两个字节了,比如字母I,本可以用01001001来存储,现在要用Unicode只能是00000000 01001001存储,而这将导致大量的空间被浪费掉。基于此,美国人创建了utf8编码,而utf8编码是一种针对Unicode的可变长字符编码方式,根据具体不同的字符计算出需要的字节,对于ASCII码范围的字符,就用一个字节,而且符号与数字的对应也是一致的,所以说utf8是兼容ASCII码表的。但是对于中文,一般是用三个字节存储的。
Go的字符与字节
byte就是字节的意思,一个字节就是8个二进制位。uint8,无符号整形,占8位,正好也是2的8次方。所以byte和 uint8 类型本质上没有区别,它表示的是 ACSII 表中的一个字符。
// byte类型
var b1 byte
b1 = 'A' // 必须是单引号
// b1 = 98 // 必须是单引号
fmt.Println(reflect.TypeOf(b1)) // 65 uint8
fmt.Printf("%c\n",b1)
fmt.Printf("%d\n",b1) // ASCII数字
fmt.Println(b1) // ASCII数字
// uint8类型
var b2 uint8
b2 = 65
// b2 = 'c'
fmt.Printf("%c\n",b2)
fmt.Printf("%d\n",b2)
fmt.Println(b2) // ASCII数字
// var b3 byte
var b3 rune
b3 = '苑'
// rune,占用4个字节,共32位比特位,所以它和 int32 本质上也没有区别。它表示的是一个 Unicode字符
fmt.Println(b3,string(b3),reflect.TypeOf(b3))
字符串
go语⾔的string是⼀种数据类型,这个数据类型占⽤16字节空间,前8字节是⼀个指针,指向字符串值的地址,后⼋个字节是⼀个整数,标识字 符串的长度;
(1)字符串的存储原理
string 数据结构:源码包src/runtime/string.go:stringStruct定义了string的数据结构:
type stringStruct struct {
str unsafe.Pointer
len int
}
其数据结构很简单:
stringStruct.str:字符串的首地址;
stringStruct.len:字符串的长度;
string数据结构跟切片有些类似,只不过切片还有一个表示容量的成员,事实上string和切片,准确的说是byte切片经常发生转换。这个后面再详细介绍。
s1 := "hello"
s2 := s1[:]
s3 := s1[1:]
fmt.Println(&s1, (*reflect.StringHeader)(unsafe.Pointer(&s1)))
fmt.Println(&s2, (*reflect.StringHeader)(unsafe.Pointer(&s2)))
fmt.Println(&s3, (*reflect.StringHeader)(unsafe.Pointer(&s3)))
字符串类型表示字符串值的集合。字符串值是一个字节序列(可能为空)。字符串是不可变的:一旦创建,就不可能改变字符串的内容。预先声明的字符串类型是string。
字符串s的长度(以字节为单位的大小)可以使用内置函数len来发现。如果字符串是常量,则长度为编译时常量。字符串的字节可以通过索引0到len(s)-1的整数来访问。取这样一个元素的地址是非法的;如果s[i]是字符串的第i个字节,&s[i]是无效的。
go语⾔指针和C/C++指针的唯⼀差别就是:go语⾔不允许对指针做算术运算(+、-、++、–)。
但是,Go提供了⼀套底层库reflect和unsafe,它们可以把任意⼀个go指针转成uintptr类型的值,然后再像C/C++⼀样对指针做算术运算,最后再还原成go类型。所以从这个⾓度上看,go指针也是可以和C/C++指针⼀样使⽤的,只是会⽐较绕,这同时也要求使⽤者⾃⼰明⽩,如果真要把指针这么⽤,那么请记得后果⾃负。
(2)字符串的使用
// 本质上,unicode是一个编码集,和ascii码相同,而utf8是编码规则
var a = '苑'
fmt.Printf("字符'苑'unicode的十进制:%d\n", a)
fmt.Printf("字符'苑'unicode的十六进制:%x\n", a)
fmt.Printf("字符'苑'unicode的二进制:%b\n", a)
var b = 0b111010001000101110010001
fmt.Printf("字符'苑'的utf8:%x\n", b)
var c = "苑abc"
fmt.Println(c) // 苑abc
for i := 0; i < len(c); i++ {
fmt.Printf("%d\n", c[i]) // 存储的字节的十进制数
}
for _, v := range c {
fmt.Printf("%d,%c\n", v, v) // 通过存储的utf8解析到unicode值和对应的符号
}
UTF-8的编码规则:
(1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。 (2)对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。
举例说明:
已知’苑’的unicode是82d1(1000001011010001),‘苑’的UTF-8编码需要三个字节,即格式是“1110xxxx 10xxxxxx 10xxxxxx”。然后,从’苑’的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,‘苑’的UTF-8编码是 “111010001 00010111 0010001”,转换成十六进制就是e88b91。
(3)字符串与字节串的转换
字节数组,就是一个数组,里面每一个元素都是字符,字符又跟字节划等号。所以字符串和字节数组之间可以相互转化。
// (1) 字符串类型(string) 转为字节串类型([]byte)
var s = "苑昊"
fmt.Println(s,reflect.TypeOf(s)) // 苑昊 string
var b = []byte(s) // 默认用uft-8进行编码
fmt.Println(b,reflect.TypeOf(b)) // [232 139 145 230 152 138] []uint8
// 可以通过代码 len([]rune(s)) 来获得字符串中字符的数量, 但使用 utf8.RuneCountInString(s) 效率会更高一点.
s := "Hello,世界"
r1 := []byte(s)
r2 := []rune(s)
fmt.Println(r1) // 输出:[72 101 108 108 111 44 32 228 184 150 231 149 140]
fmt.Println(r2) // 输出:[72 101 108 108 111 44 32 19990 30028]
// 统计字节个数
fmt.Println(len(r1))
// 统计字符个数
fmt.Println(len(r2))
fmt.Println(utf8.RuneCountInString(s))
// (2) byte转为string
fmt.Println(string(b))
var data = []byte{121,117,97,110}
fmt.Println(string(data)) // yuan
这里的转化不是将string结构体中指向的byte切片直接做赋值操作,而是通过copy实现的,在数据量比较大时,这里的转化会比较耗费内存空间。
(4)练习
将字符串 “hello” 转换为 “cello”
s := "hello"
c := []byte(s)
c[0] = 'c'
s2 := string(c) //s2 == "cello"
将字符串 “hello” 反转
func reverseString(s []byte) []byte {
var i, j = 0, len(s) - 1
for i < j {
s[i], s[j] = s[j], s[i]
i++
j--
}
return s
}
读写文件
.1、打开文件
os.Open()函数能够打开一个文件,返回一个*File和一个err。
//打开文件
file, err := os.Open("./满江红")
if err != nil {
fmt.Println("err: ", err)
}
//关闭文件句柄
defer file.Close()
2、读文件
package main
import (
"bufio"
"fmt"
"io"
"io/ioutil"
"os"
)
func readBytes(file *os.File) {
var b = make([]byte, 3)
n, err := file.Read(b)
if err != nil {
fmt.Println("err:", err)
return
}
fmt.Printf("读取字节数:%d\n", n)
fmt.Printf("切片值:%v\n", b)
fmt.Printf("读取内容:%v\n", string(b[:n]))
}
func readLines(file *os.File) {
reader := bufio.NewReader(file)
for {
// (1) 按行都字符串
strs, err := reader.ReadString('\n') // 读取到换行符为止,读取内容包括换行符
fmt.Print(err, strs)
// (2) 按行都字节串
// bytes, err := reader.ReadBytes('\n')
// fmt.Print(bytes)
// fmt.Print(string(bytes))
if err == io.EOF { //io.EOF 读取到了文件的末尾
// fmt.Println("读取到文件末尾!")
break
}
}
}
func readFile() {
content, err := ioutil.ReadFile("满江红") //包含了打开文件和读取整个文件,适用于较小文件
if err != nil {
fmt.Println("read file failed, err:", err)
return
}
fmt.Print(string(content))
}
func main() {
//打开文件
file, err := os.Open("满江红") // 相对路径或者绝对路径
if err != nil {
fmt.Println("err: ", err)
}
//关闭文件句柄
defer file.Close()
// (1) 按字节读取数据
// readBytes(file)
// (2) 按行读取文件
// readLines(file)
// (3) 读取整个文件
// readFile()
}
3、写文件
OpenFile是一个更一般性的文件打开函数,大多数调用者都应用Open或Create代替本函数。它会使用指定的选项(如O_RDONLY等)、指定的模式(如0666等)打开指定名称的文件。如果操作成功,返回的文件对象可用于I/O。如果出错,错误底层类型是*PathError。
func OpenFile(name string, flag int, perm FileMode) (file *File, err error) // ⽂件路径、打开模式、⽂件权限
/*
os.O_RDONLY: 只读模式(read-only)
os.O_WRONLY: 只写模式(write-only)
os.O_RDWR : 读写模式(read-write)
os.O_APPEND: 追加模式(append)
os.O_CREATE: ⽂件不存在就创建(create a new file if none exists.)
os.O_TRUNC: 打开并清空⽂件(必须有写权限)
os.O_EXCL: 如与 O_CREATE ⼀起⽤,构成⼀个新建⽂件的功能,它要求⽂件必须不存在(used with O_CREATE, file must not exist)
os.O_SYNC:同步⽅式打开,即不使⽤缓存,直接写⼊硬盘
*/
(1)只写模式
package main
import (
"bufio"
"fmt"
"io/ioutil"
"os"
)
func writeBytesOrStr(file *os.File) {
str := "满江红666\n"
//写入字节切片数据
file.Write([]byte(str))
//直接写入字符串数据
file.WriteString("怒发冲冠,凭栏处、潇潇雨歇。")
}
func writeByBufio(file *os.File) {
writer := bufio.NewWriter(file)
//将数据先写入缓存,并不会到文件中
writer.WriteString("大浪淘沙\n")
// 必须flush将缓存中的内容写入文件
// writer.Flush()
}
func writeFile() {
str := "怒发冲冠,凭栏处、潇潇雨歇。"
err := ioutil.WriteFile("满江红", []byte(str), 0666)
if err != nil {
fmt.Println("write file failed, err:", err)
return
}
}
func main() {
file, err := os.OpenFile("满江红.txt", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
fmt.Println("open file failed, err:", err)
return
}
defer file.Close()
// 写字节或者字符串
writeBytesOrStr(file)
// flush写
writeByBufio(file)
// 写文件
writeFile()
}
0777:-rwxrwxrwx,创建了一个普通文件,所有人拥有所有的读、写、执行权限 0666:-rw-rw-rw-,创建了一个普通文件,所有人拥有对该文件的读、写权限,但是都不可执行 0644:-rw-r–r–,创建了一个普通文件,文件所有者对该文件有读写权限,用户组和其他人只有读权限,没有执行权限
(2)读写模式
读取一个文件每一行内容,并追加一行该行的字符个数
package main
import (
"bufio"
"fmt"
"io"
"os"
"strings"
)
func main() {
file, err := os.OpenFile("读写满江红", os.O_CREATE|os.O_RDWR|os.O_APPEND, 0666)
if err != nil {
fmt.Println("open file failed, err:", err)
return
}
defer file.Close()
reader := bufio.NewReader(file)
writer := bufio.NewWriter(file)
for true {
// (1) 按行都字符串
strs, err := reader.ReadString('\n') // 读取到换行符为止,读取内容包括换行符
content := strings.Trim(strs, "\n")
s := fmt.Sprintf("\n该行长度为%d,内容为:%s", len([]rune(content)), content)
// (2) 将行数记录追加进入文件
writer.WriteString(s)
writer.Flush()
if err == io.EOF {
break
}
}
}
其它文件操作
(1) 删除文件
os.Remove(fname)
(2) 创建目录
dname :=“rain”
os.Mkdir(dname,os.ModeDir|os.ModePerm)
(3)获取文件信息
通过os.Stat方法,我们可以获取文件的信息,比如文件大小、名字等。
func main() {
f,err:=os.Stat("满江红")
if err ==nil {
fmt.Println("name:",f.Name())
fmt.Println("size:",f.Size())
fmt.Println("is dir:",f.IsDir())
fmt.Println("mode::",f.Mode())
fmt.Println("modTime:",f.ModTime())
}
}
以上就是可以获取到的文件信息,还包括判断是否是目录,权限模式和修改时间。所以我们对于文件的信息获取要使用os.Stat函数,它可以在不打开文件的情况下,高效获取文件信息。
os.Stat函数有两个返回值,一个是文件信息,一个是err,通过err我们可以判断文件是否存在。首先,err==nil的时候,文件肯定是存在的;其次err!=nil的时候也不代表不存在,通过err是否等于os.IsNotExist来判断一个文件不存在。