字符 转 ascii

'a'.ord # => 97

"a".unpack('c') #=> 97 `c` 提取一个字符作为整数

"string".each_byte do |c|
    puts c
end

'hello world'.each_byte.to_a
[104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]

'hello world'.bytes
[104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]

Ascii 转 字符

97.chr # => "a"

'hello world'.bytes # => [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]
[104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100].map{|m|m.chr}
# => ["h", "e", "l", "l", "o", " ", "w", "o", "r", "l", "d"]

字符转二进制

"aa".unpack('b8B8')  #=> ["10000110", "01100001"]

B 从每个字符中提取位(首先是最高有效位)

b 从每个字符中提取位(首先是最低有效位)

bufio.Peek(n)返回前N个未读字节 ,不会更改已读计数的值。在方法调用后,要查看返回参数error是否为nil,以确保操作满足要求

func (b *Reader) Peek(n int) ([]byte, error) {
	if n < 0 {
		return nil, ErrNegativeCount
	}

	b.lastByte = -1
	b.lastRuneSize = -1

	// 当未读字节数小于n,且缓冲区不满(b.w-b.r < len(b.buf)),
	// 即缓冲区中从头到尾必须都是未读字节才算是缓冲区已经满
	// 且 b.err 为nil,这三者都满足时,开始调用b.fill()填充缓冲区
	// fill()方法会把未读数据移动到缓冲区头部,并把后面空出来的部分写满
	// for 保证了至少可以把缓冲区填满
	for b.w-b.r < n && b.w-b.r < len(b.buf) && b.err == nil {
		b.fill() // b.w-b.r < len(b.buf) => buffer is not full
	}

	// 当要读取的字节数大于缓冲区长度时,返回所有未读节字,并附带错误信息
	if n > len(b.buf) {
		return b.buf[b.r:b.w], ErrBufferFull
	}

	// 0 <= n <= len(b.buf)
	var err error
	// 当要读取的字节数大于所有未读节字数时,返回所有未读节字,并附带错误信息
	if avail := b.w - b.r; avail < n {
		// not enough data in buffer
		n = avail
		err = b.readErr()
		if err == nil {
			err = ErrBufferFull
		}
	}
	// 当 n 小于 未读字节数时,程序直接跳到这里
	return b.buf[b.r : b.r+n], err
}

r 是指针类型接收者,可以对其进行解引赋值。结构体的0值不是nil,而是其各个成员的0值

package main

import "fmt"

func main() {
	newReader()
}

type reader struct {
	buf  []byte
	r, w int
}

func newReader() {
	r := new(reader)
	fmt.Println(r) // &{[] 0 0}
	r.reset(make([]byte, 5))
	fmt.Println(r) // &{[0 0 0 0 0] -1 -1}
}

func (r *reader) reset(buf []byte) {
	*r = reader{buf: buf, r: -1, w: -1}
}

在 mac OS 上安装

brew install gdb

也可以从源码进安装,官网,目前GDB对下列语言进行了支持

Ada
Assembly
C
C++
D
Fortran
Go
Objective-C
OpenCL
Modula-2
Pascal
Rust

安装后还不能直接使用,MacOS 对安全性有效高的要求,不允许没有许可的程序运行其它程序

gdb helloworld
(gdb) run

Unable to find Mach task port for process-id 798: (os/kern) failure (0x5).
 (please check gdb is codesigned - see taskgated(8))

提示please check gdb is codesigned - see taskgated(8),原因是还没有为gdb签名,需要在系统的Keychain中创建用于签名的证书

创建certificate

步骤如下:

  1. Launch Keychain Access application: Applications > Utilities > Keychain Access.
  2. From the Keychains list on the left, right-click on the System item and select Unlock Keychain “System”.
  3. From the toolbar, go to Keychain Access > Certificate Assistant > Create a Certificate.
  4. Choose a name (e.g. gdb-cert).
  5. Set Identity Type to Self Signed Root.
  6. Set Certificate Type to Code Signing.
  7. Check the Let me override defaults checkbox.
  8. At this point, you can go on with the installation process until you get the Specify a Location For The Certificate dialogue box. Here you need to set Keychain to System. Finally, you can click on the Create button.
  9. After these steps, you can see the new certificate under System keychains. From the contextual menu of the newly created certificate (right-click on it) select the Get info option. In the dialogue box, expand the Trust item and set Code signing to Always Trust.
  10. Then, from the Keychains list on the left, right-click on the System item and select Lock Keychain “System”.
  11. Finally, reboot your system.

gdb签名

在创建你自己的证书后,就可以给gdb进行签名了,在签名前要先配置gdb-entitlement.xml文件,它允许MacOs信任gdb

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>com.apple.security.cs.allow-jit</key>
    <true/>
    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
    <true/>
    <key>com.apple.security.cs.allow-dyld-environment-variables</key>
    <true/>
    <key>com.apple.security.cs.disable-library-validation</key>
    <true/>
    <key>com.apple.security.cs.disable-executable-page-protection</key>
    <true/>
    <key>com.apple.security.cs.debugger</key>
    <true/>
    <key>com.apple.security.get-task-allow</key>
    <true/>
</dict>
</plist>

在命令行运行

codesign --entitlements gdb-entitlement.xml -fs gdb-cert "$(which gdb)"

-fs: 要填写你刚刚生成的证书的名子,$(which gdb): 是gdb的安装路径

接着在home目录创建~/.gdbinit文件,并写入如下内容

echo "set startup-with-shell off" >> ~/.gdbinit

编译文件

为了解决No symbol table is loaded问题,可以在编译时加入-ggdb

gcc hello_world.c -o hello_world -ggdb

# 使用示例
gdb hello_world
(gdb) list
1	#include <stdio.h>
2
3	int my_strlen(char *string);
4
5	int main(int argc, char const *argv[])
6	{
7	  // char *string;
8	  // string = "hello world haha";
9	  char string[] = "hello world haha";
10	  printf("The string '%s' len is \"%d\"\n", string, my_strlen(string));
(gdb) run
Starting program: /Users/ga/m/book/c-lang/ch02-basic/hello_world
[New Thread 0x2603 of process 6002]
[New Thread 0x1803 of process 6002]
warning: unhandled dyld version (16)
The string 'hello world haha' len is "16"
[Inferior 1 (process 6002) exited normally]

使用gdb调试golang

本博没有成功,一直提示错误No symbol table is loaded. Use the "file" command. ,后来查看了Go的官方文档,官方建议使用Delve进行调试。官方原文

Note that Delve is a better alternative to GDB when debugging Go programs built with the standard toolchain. It understands the Go runtime, data structures, and expressions better than GDB. Delve currently supports Linux, OSX, and Windows on amd64. For the most up-to-date list of supported platforms, please see the Delve documentation.

并且在官方给出的示例中,也不保证你照着运行也可以成功

In short, the instructions below should be taken only as a guide to how to use GDB when it works, not as a guarantee of success. Besides this overview you might want to consult the GDB manual.

最后祝你一切顺利

Git 创建标签

Git 标签分两种

  • 附注标签: annotated: 是存储在 Git 数据库中的一个完整对象, 它们是可以被校验的,其中包含打标签者的名字、电子邮件地址、日期时间, 此外还有一个标签信息
  • 轻量级标签: lightweight: 像一个不会改变的分支——它只是某个特定提交的引用。在实践中不推荐使用

打标签

# 附注标签
git tag -a v0.0.1 -m "first release try"

# 发布标签
# 标签不会随push而自动被push到远程,需要单独push
git push origin v0.0.1

# 给某个commit单独打tag
# 这种操作多用于给历史commit打tag,原因可能是当时没有打
git tag -a v0.0.2 2234093

标签操作

git tag

# 查看某个tag的具体内容
git show v0.0.1
git cat-file -p v0.0.1

# 删除标签
git tag -d v0.0.1
=> Deleted tag 'v0.0.1' (was 79db751)

# 删除远程标签
git push origin :v0.0.1

# 查看tag的sha1
cat .git/refs/tags/v5.4.1 # 在标签被删除的时候会显示这个值
# => 79db751

签名tag

前置条件:要先配置GPG

# 强制给tag 签名
# 直接编辑  ~/.gitconfig 也可以
git config --global tag.forcesignannotated true

fmt格式化含有0值的string时,是如何显示的

func main() {
	buf := make([]byte, 10)
	buf[8] = 'A'
	copy(buf, []byte("hello"))
	fmt.Printf("cap(%d),len(%d)\n", cap(buf), len(buf))
	fmt.Println("buf:", buf)
	fmt.Printf("buf:%q\n", buf)
	fmt.Println("buf:", string(buf))

	str := string(buf)
	str = str + " world"
	fmt.Println(len(str)) // => 16
	fmt.Println(str)      // => helloA world
}

/* output
cap(10),len(10)
buf: [104 101 108 108 111 0 0 0 65 0]
buf:"hello\x00\x00\x00A\x00"
buf: helloA
16
helloA world
*/

1. 使用fmt打印string(buf)虽然显示为helloA,看上去字符数变少了,只有6个(0值没有显示,其ascii码是不可打印码),但底层数据没变,依然是[104 101 108 108 111 0 0 0 65 0]

2. 格式化参数%q直接对0值进行了输出hello\x00\x00\x00A\x00,它打印了string中的每一个字节,包括不可打印的字节

3. str + " world"的长度为16,也证明了str中的0值没有被去除(10 + 6 = 16)

4.使用fmt查看数据”长什么样子”有时候是不准确的

io.Writer接口实现要求

开始以为只是实现了Write方法就行,看了源码发现还有很多细节上的要求

// src/io/io.go
type Writer interface {
	Write(p []byte) (n int, err error)
}

1.要把 p写入实现类型的底层数据中

2.要返回实际写入的长度和在写入时遇到的错误,如果实际写入的长度小于len(p),返回的err必须是非nil

3. p不能在方法内部被修改,即使临时性的也不可以

4. 不能对p进行保存

申请GitHub应用

1. 注册一个新应用

https://github.com/settings/applications/new 有三项内容必须填写

Application name:这个随意填写
Homepage URL:你的网站的URL地址,包含http部分。
Authorization callback URL:你的网站的URL地址,包含http部分,和上面的 Homepage URL 一致就行

2. Homepage URLAuthorization callback URL的填写

  1. 如果你的网站是host在github上的,是通过github分配给你的网址进行访问的,直接填写github分配给你的网站就可以,类似 yourgithub.github.io
  2. 如果你有自己的域名,但网站内容是host在github上的,是通过cname的方式访问的,那么这两项都填你的域名
  3. 如果你有自己的域名,也有自己的站点,那么这两项都填你的域名

在调试阶段,可以把这两项设置为你的本地地址,这样方便测试,而不用一次又一次的部署网站,等测试好之后再统一部署。

3. 生成Client信息

点击 “register application”完成注册,系统会生成Client IDClient secrets(需要手动点一下旁边的按钮)

配置Gitalk

通过Next主题进行配置

如果你的网点是用Hexo驱动,且主题是Next,这个主题已经为我们配置了gitalk,只需配置即可

gitalk:
  enable: true
  github_id: 你的Github登录账号
  repo: gitalk 你的 Github仓库的名称
  client_id: 上面刚刚申请的 Client ID
  client_secret: 上面刚刚申请的 Client Secret
  admin_user: 你的Github登录账号
  distraction_free_mode: false
  proxy: https://cors-anywhere.azm.workers.dev/https://github.com/login/oauth/access_token
  language:

使用官网配置

官网地址:https://github.com/gitalk/gitalk

const gitalk = new Gitalk({
  clientID: 'GitHub Application Client ID',
  clientSecret: 'GitHub Application Client Secret',
  repo: 'GitHub repo',      // The repository of store comments,
  owner: 'GitHub repo owner',
  admin: ['GitHub repo owner and collaborators, only these guys can initialize github issues'],
  id: location.pathname,      // Ensure uniqueness and length less than 50
  distractionFreeMode: false  // Facebook-like distraction free mode
})

gitalk.render('gitalk-container')

参数说明

注:官网配置的参数和Hexo主题Next的参数是通用的

  • repo: 要求填写你的Github仓库的名称,这个仓库可以是你的github上的任意一个,但必须是公开的。评论内容会以 issue的形式保存到仓库的issues中,所以建议为评论单独新建一个仓库
  • admin: 指定了可以初始化文章评论的github用户,一般只填写你自己就可以了
  • id: 文章的URL路径,不包括域名,这个路径要求必须是唯一的,且不能超过50个字符(如果超过了,可以使用md5等工具对路径进行摘要缩短长度即可)。每一扁文章的评论和这个路径相关联,所以即使域名不同,只要文章的URL一样,就会显示相关联的评论。在Next主题中是自动配置的Hexo主题Next的参数定义在layout/_third-party/comments/gitalk.njk
  • distractionFreeMode:是否是聚精会神模式,即评论框在写评论的时候,周围是否变成黑

遇到的坑

1. Error: Not Found:一般是仓库名称没有正确设置,请依据上面的参数说明部分进行设置

2. Related Issues not found:未找到相关的issue进行评论,出现这种情况是因为文章评论还未初始化,只需要点击下面的按钮,登录 GitHub 账户即可

3. 评论区一直加载不上:配置中的proxy地址不正确或已经失效。官网默认地址是https://cors-anywhere.azm.workers.dev/https://github.com/login/oauth/access_token。如果你使用的是Next主题,请确保也是这个地址

4. 评论时是黑色的:设置distractionFreeMode为false

对于Golang的初学者,Truncate方法的行为使人迷惑,它不是从 buffer的头部截取 n 个字节,而是丢弃除了前n个未读取的字节之外的所有未读取的字节,也是就只保留已读取的字节和还没有读取的前n个字节。这种逻辑确实让人不太适应

// Truncate discards all but the first n unread bytes from the buffer
// but continues to use the same allocated storage.
// It panics if n is negative or greater than the length of the buffer.
func (b *Buffer) Truncate(n int) {
	if n == 0 {
		b.Reset()
		return
	}
	b.lastRead = opInvalid
	if n < 0 || n > b.Len() {
		panic("bytes.Buffer: truncation out of range")
	}
	b.buf = b.buf[:b.off+n]
}

从源码的注释中还可以看到,截取后使用原 buffer保存保留下来的节字,不再额外占用内存空间。同时 n 的大小不能超过未读取字节的长度(第10行)

func truncate() {
  var buffer bytes.Buffer
  str := "Simple byte buffer for marshaling data."
  buffer.WriteString(str)
  p := make([]byte, 8)
  buffer.Read(p) // Simple b
  fmt.Println(string(p))

  buffer.Truncate(10)    // 截断到前10个未读节字 Simple byte buffer
  buffer.Read(p)         // 读取8个字节
  fmt.Println(string(p)) // yte buff
}
  • p := make([]byte, 8)先读取了8个字节
  • buffer.Truncate(10) 再从没有读取的字节中取出10个,丢掉其余未读取的字节,把已读取的8个字节和取出的10个字节保存到 buffer中,此时buffer中的内容为Simple byte buffer
  • buffer.Read(p) 再读取8个字节,注意:这里不是从 buffer 的头部读取,而是从未读取处开始读取,即从y开始读取

一些基础知识

  • 字节对齐
  • unsafe.Sizeof
  • unsafe.Offsetof
  • 内存空洞

字节对齐

可以使计算机在加载和保存数据时,更加的有效率

通常情况下布尔和数字类型需要对齐到它们本身的大小(最多8个字节),其它的类型对齐到机器字大小

unsafe.Sizeof

返回操作数在内存中的字节大小,参数可以是任意类型的表,但不会对表达式进行求值(不求值也能知道大小,好神奇呀)

unsafe.Sizeof 返回的大小只包含数据结构中固定的部分。如果结构体含有指针字段,不包括针指向的内容。Go语言中非聚合类型通常有一个固定的大小,而聚合类型没有固定的大小,比如 结构体类型和数组类型

unsafe.Offsetof

函数的参数必须是一个字段 x.f,然后返回 f 字段相对于 x 起始地址的偏移量,包括可能的空洞

内存空洞

一个聚合类型(结构体或数组)的大小至少是所有字段或元素大小的总和,或者更大。因为可能存在内存空洞,内存空洞是编译器自动添加的没有被使用的内存空间,用于保证后面每个字段或元素的地址相对于结构或数组的开始地址能够合理地对齐。内存空洞可能会存在一些随机数据,可能会对用unsafe包直接操作内存的处理产生影响

结构体内存布局

设:机器字大小为8个字节

产生的空洞

var x struct {
  a bool
  b int16
  c []int
}
/* output
Sizeof(x)   = 32  Alignof(x)   = 8
Sizeof(x.a) = 1   Alignof(x.a) = 1 Offsetof(x.a) = 0
Sizeof(x.b) = 2   Alignof(x.b) = 2 Offsetof(x.b) = 2
Sizeof(x.c) = 24  Alignof(x.c) = 8 Offsetof(x.c) = 8
*/
  • x 占用内存大小为 32字节
  • x.c字段是一个切片,占用24个字节(3个机器字),c.data, c.len, c.cap 分别用 8个字节(1 个机器字)
  • x.a + x.b 总共占用 3字节。用x的占用总字节数 32 - (1 + 2 + 24) = 5, 说明有5字节的内存空洞
  • 由于x.c占用了3个机器字,所以空洞不是它产生的
  • x.a + x.b = 3字节,不满一个机器字(8字节),所以a和b之间,b和c之间产生了总共5字节的空洞

字段偏移分析

  • Offsetof(x.a) = 0 说明 字段 a 处在结构的起始处,a与结构体起始处没有偏移(与起始处没有空洞)
  • Offsetof(x.b) = 2 说明 字段 b 相对于结构体 起始处 偏移了 2 字节,而Sizeof(x.a) = 1说明 x.a 占用只占用了1字节,但 b 偏移了 2 字节,说明b与a之间有 1 字节的空洞,否则 b 只应该偏移 1 字节,即x.a的大小。
  • 那么 a与b之间,总共是4字节的大小: x.a 1字节 + 空洞 1 字节 + x.b 2字节
  • 如果 x.c 与 x.b之前没有空洞,则x.c只应该偏移4字节,但实际却偏移了8字节,则 说明 x.c 与 x.b 之间 存在 8 - 4 = 4 字节的空洞
  • 所以 x 结构体的内存 分布是: x.a(1)____空洞(1)____x.b(2)____空洞(4)____x.c(24)
  • 对齐方式:按一个机器字对齐的

layout of struck

结构体字段顺序

Go 语言中,结构内部字段的声明顺序和它们在内存中的顺序可能是不一样的。一个编译器可以随意地重新排列每个字段的内存位置,有效的包装可以使数据结构更加紧凑,从而节省内存空间

内存占用

不同结构体相同字段占用内存大小也会不一样,虽然 s1,s2,s3 有着相同的字段,但s1占用了较多的内存空间

var s1 = struct {a bool;b float64;c int16}{} // 3 words
var s2 = struct {a float64;b int16;c bool}{} // 2 words
var s3 = struct {a bool;b int16;c float64}{} // 2 words

s1占用空间

sizeof(s1) =   24 Alignof(s1) =    8
Sizeof(s1.a) =  1 Alignof(s1.a) =  1 Offsetof(s1.a) =  0
Sizeof(s1.b) =  8 Alignof(s1.b) =  8 Offsetof(s1.b) =  8
Sizeof(s1.c) =  2 Alignof(s1.c) =  2 Offsetof(s1.c) =  16

综上: s1.a 与 s1.b 之间有 7 字节 的空洞,s1.c与结构体结束处(尾部)有 6 字节的空洞

所以: s1 总字节数是 1 + 8 + 2 + (7 + 6) 空洞 = 24 byte,即3个机器字,可以看出 s1 的字段与字段之间,排列的并不是很紧凑,有较大空洞,造成了内存的浪费

对齐方式:按一个机器字对齐的

|-a-|----------holes------------| 8字节,即一个机器字
|---------------b---------------| 8字节,即一个机器字
|---c---|---------holes---------| 8字节,也可看出是按一个机器字对齐的

s2 占用空间

sizeof(s2) =   16 Alignof(s2) =    8
Sizeof(s2.a) =  8 Alignof(s2.a) =  8 Offsetof(s2.a) =  0
Sizeof(s2.b) =  2 Alignof(s2.b) =  2 Offsetof(s2.b) =  8
Sizeof(s2.c) =  1 Alignof(s2.c) =  1 Offsetof(s2.c) =  10

综上: s2.a 的大小是一个机器字,本身就是对齐的,且是所有字段中长度最大的,与 s2.b之间没有空洞,s2.c紧贴s2.b,它们之间也没有空洞,s2.c与结构体结束处(尾部)有 5(8-2+1) 字节的空洞

所以: s2 总字节数是 8 + 2 + 1 + (5) 空洞 = 16 byte,即2个机器字,可以看出 s2 的字段与字段之间,排列是很紧凑,可以大大节省内存空间

对齐方式:按一个机器字对齐的

|---------------a---------------| 8字节,即一个机器字
|---b---|-c-|-------holes-------| 8字节,即一个机器字

s3占用空间

sizeof(s3) =   16 Alignof(s3) =    8
Sizeof(s3.a) =  1 Alignof(s3.a) =  1 Offsetof(s3.a) =  0
Sizeof(s3.b) =  2 Alignof(s3.b) =  2 Offsetof(s3.b) =  2
Sizeof(s3.c) =  8 Alignof(s3.c) =  8 Offsetof(s3.c) =  8

对齐方式:按一个机器字对齐的

s3 布局与 s2 相似,可以看成是上下两层对调了,但排列是很紧凑的,也是2个机器字

|-a-|---b---|-------holes-------| 8字节,即一个机器字
|---------------c---------------| 8字节,也可看出是按一个机器字对齐的

相关问题

未来的Go语言编译器应该会默认优化结构体的顺序,当然应该也能够指定具体的内存布局,相同讨论请参考 Issue10014