前言
作为开发者,访问 GitHub 慢或打不开是日常痛点。常见方案是手动查找 IP 然后修改 hosts 文件,但 IP 会不定期变更,手动维护非常麻烦。
于是我用 Go 写了一个小工具 GitHubHosts,自动完成以下流程:
DNS 解析 → IP 可达性检测 → 选择最快 IP → 生成 hosts 文件
全程多线程并发,34 个域名几秒内完成解析和筛选。
核心功能
| 功能 | 说明 |
|---|---|
| 🌐 自定义 DNS 服务器 | 通过 -dns 参数指定,默认 1.1.1.1 |
| 📄 外部域名文件 | 读取 source.txt,不存在则使用内置域名列表 |
| 🔍 域名自动去重 | 支持外部文件传入时有重复域名,自动去重 |
| ⚡ 多线程并发 | 所有域名同时解析,同域名多 IP 同时探测 |
| 🏆 最快 IP 选取 | TCP 443 探测延迟,选响应最快的 IP |
| 📝 自动生成 hosts | 输出 hosts.txt |

快速使用
编译
bash
go build -o GitHubHosts.exe main.go
运行
bash
# 使用默认 DNS (1.1.1.1)
./GitHubHosts.exe
# 指定 DNS 服务器
./GitHubHosts.exe -dns 8.8.8.8
自定义域名
在同级目录创建 source.txt,每行一个域名,支持 # 注释:
text
# GitHub 域名列表
github.com
api.github.com
raw.githubusercontent.com
gist.github.com
文件不存在或为空时,自动使用内置的 34 个 GitHub 相关域名。
输出效果
控制台输出:
####################
github.com -> 20.205.243.166 (delay: 12.3ms)
api.github.com -> 20.205.243.168 (delay: 15.7ms)
raw.githubusercontent.com -> 185.199.109.133 (delay: 8.1ms)
...
# Last Update Time : 2026-06-29 10:30:45
####################
生成的 hosts.txt:
# ####################Github Start####################
20.205.243.166 github.com
20.205.243.168 api.github.com
185.199.109.133 raw.githubusercontent.com
...
# Last Update Time : 2026-06-29 10:30:45
# Blog: https://blog.csdn.net/ccboy2009
# ####################Github End####################
设计思路
1. DNS 解析:自定义服务器
通过 nslookup 命令解析域名,支持指定 DNS 服务器:
go
dnsServer := flag.String("dns", "1.1.1.1", "DNS server IP address")
flag.Parse()
不传入时默认使用 Cloudflare 的 1.1.1.1,也可用 8.8.8.8(Google)或其他。
2. 解析输出处理:兼容 GBK 编码
Windows 下 nslookup 的输出可能是 GBK 编码,中文关键词匹配会失败。策略是:直接用正则提取所有 IP 地址,去掉 DNS 服务器自身地址,剩余即为解析结果:
go
func parse_nslookup_output(output string, dnsServer string) []string {
ipRegex := regexp.MustCompile(`(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})`)
matches := ipRegex.FindAllStringSubmatch(output, -1)
var ips []string
for _, m := range matches {
ip := m[1]
if ip == dnsServer { // 跳过 DNS 服务器自身
continue
}
ips = append(ips, ip)
}
return ips
}
3. IP 可达性检测:TCP 443 探测
为什么用 TCP 443 而不是 ICMP ping?
- 这些域名写入 hosts 后,通过浏览器 HTTPS 访问,走的就是 443 端口
- 很多 CDN 节点禁用了 ICMP,ping 不通但 443 端口实际可用
- TCP 探测能真实反映服务可用性
go
func ping_ip(ip string, timeout time.Duration) ipPingResult {
start := time.Now()
conn, err := net.DialTimeout("tcp", ip+":443", timeout)
elapsed := time.Since(start)
if err != nil {
return ipPingResult{ip: ip, ok: false}
}
conn.Close()
return ipPingResult{ip: ip, delay: elapsed, ok: true}
}
4. 最快 IP 选取
一个域名通常解析出多个 IP,并发探测后按延迟排序,取最快的一个:
go
sort.Slice(reachable, func(i, j int) bool {
return reachable[i].delay < reachable[j].delay
})
best := reachable[0]
降级策略:如果所有 IP 都不可达,仍使用 DNS 解析到的第一个 IP,避免丢失域名。
5. 两级并发
┌─────────────────────────────────────────────────┐
│ 域名级并发:所有域名同时 DNS 解析 + IP 探测 │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ github.com │ │ api.github│ │ raw.github │ │
│ │ goroutine │ │ goroutine│ │ goroutine │ │
│ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ │
│ │ │ │ │
│ IP级并发:同域名多IP同时探测 │
│ ┌─────┴─────┐ ┌─────┴─────┐ ┌─────┴─────┐ │
│ │IP1│IP2│IP3│ │IP1│IP2│IP3│ │IP1│IP2│IP3│ │
│ └───────────┘ └───────────┘ └───────────┘ │
└─────────────────────────────────────────────────┘
- 域名级 :使用
sync.WaitGroup+ goroutine,所有域名同时开始解析 - IP 级:每个域名下的多个 IP 同时 TCP 探测
串行处理 34 个域名需要逐个等待,并发后总耗时 ≈ 最慢的单个域名耗时。
6. 域名去重
外部文件可能存在重复域名,使用 map 去重并保持原有顺序:
go
func deduplicateDomains(domains []string) []string {
seen := make(map[string]bool)
var result []string
for _, d := range domains {
d = strings.TrimSpace(d)
if d != "" && !seen[d] {
seen[d] = true
result = append(result, d)
}
}
return result
}
7. 输出顺序保持
并发结果通过 channel 收集到 map,再按原始域名顺序遍历输出,保证 hosts 文件顺序稳定:
go
ipMap := make(map[string]string)
for r := range results {
ipMap[r.domain] = r.ip
}
for _, v := range url_list {
if ip, ok := ipMap[v]; ok {
result += fmt.Sprintf("%-39s%s\n", ip, v)
}
}
Go 时间格式小贴士
Go 的时间格式化与其他语言不同,不是用 YYYY-MM-DD 这种符号,而是用参考时间 Mon Jan 2 15:04:05 MST 2006 的各组成部分来指代:
| 格式 | 含义 | 记忆 |
|---|---|---|
2006 |
年 | 6 |
01 |
月 | 1 |
02 |
日 | 2 |
15 |
时(24h) | 3 |
04 |
分 | 4 |
05 |
秒 | 5 |
记住 1-2-3-4-5-6 就能写出任何格式:
go
time.Now().Format("2006-01-02 15:04:05")
// 输出: 2026-06-29 10:30:45
完整源码
go
package main
import (
"flag"
"fmt"
"io/ioutil"
"net"
"os"
"os/exec"
"regexp"
"sort"
"strings"
"sync"
"time"
)
func main() {
dnsServer := flag.String("dns", "1.1.1.1", "DNS server IP address")
flag.Parse()
defaultDomains := []string{
"alive.github.com",
"live.github.com",
"github.githubassets.com",
"central.github.com",
"desktop.githubusercontent.com",
"assets-cdn.github.com",
"camo.githubusercontent.com",
"github.map.fastly.net",
"github.global.ssl.fastly.net",
"gist.github.com",
"github.io",
"github.com",
"github.blog",
"api.github.com",
"raw.githubusercontent.com",
"user-images.githubusercontent.com",
"favicons.githubusercontent.com",
"avatars5.githubusercontent.com",
"avatars4.githubusercontent.com",
"avatars3.githubusercontent.com",
"avatars2.githubusercontent.com",
"avatars1.githubusercontent.com",
"avatars0.githubusercontent.com",
"avatars.githubusercontent.com",
"codeload.github.com",
"github-cloud.s3.amazonaws.com",
"github-com.s3.amazonaws.com",
"github-production-release-asset-2e65be.s3.amazonaws.com",
"github-production-user-asset-6210df.s3.amazonaws.com",
"github-production-repository-file-5c1aeb.s3.amazonaws.com",
"githubstatus.com",
"github.community",
"github.dev",
"media.githubusercontent.com",
}
url_list := deduplicateDomains(loadDomains("source.txt", defaultDomains))
result := "# ####################Github Start####################\n"
fmt.Printf("####################\n")
// 多线程并发解析所有域名
type domainIP struct {
domain string
ip string
}
var wg sync.WaitGroup
results := make(chan domainIP, len(url_list))
for _, v := range url_list {
wg.Add(1)
go func(domain string) {
defer wg.Done()
ip := get_fastest_ip(domain, *dnsServer)
if ip != "" {
results <- domainIP{domain: domain, ip: ip}
}
}(v)
}
// 等待所有任务完成后关闭 channel
go func() {
wg.Wait()
close(results)
}()
// 收集结果并按原始域名顺序输出
ipMap := make(map[string]string)
for r := range results {
ipMap[r.domain] = r.ip
}
for _, v := range url_list {
if ip, ok := ipMap[v]; ok {
result += fmt.Sprintf("%-39s%s\n", ip, v)
fmt.Printf("%-39s%s\n", ip, v)
}
}
result += fmt.Sprintf("# Last Update Time : %s \n", time.Now().Format("2006-01-02 15:04:05"))
result += "# Blog: https://blog.csdn.net/ccboy2009 \n"
fmt.Printf("# Last Update Time : %s \n", time.Now().Format("2006-01-02 15:04:05"))
result += "# ####################Github End####################\n"
fmt.Printf("####################")
//保存文件
ioutil.WriteFile(`hosts.txt`, []byte(result), 0666)
content := read_tmp()
content = strings.Replace(content, "12345", result, 1)
write_file(content)
}
func loadDomains(filePath string, defaultList []string) []string {
data, err := ioutil.ReadFile(filePath)
if err != nil {
fmt.Printf("source.txt not found, using default domains\n")
return defaultList
}
var domains []string
for _, line := range strings.Split(string(data), "\n") {
line = strings.TrimSpace(line)
if line != "" && !strings.HasPrefix(line, "#") {
domains = append(domains, line)
}
}
if len(domains) == 0 {
fmt.Printf("source.txt is empty, using default domains\n")
return defaultList
}
fmt.Printf("loaded %d domains from %s\n", len(domains), filePath)
return domains
}
// deduplicateDomains 对域名列表去重,保持原有顺序
func deduplicateDomains(domains []string) []string {
seen := make(map[string]bool)
var result []string
for _, d := range domains {
d = strings.TrimSpace(d)
if d != "" && !seen[d] {
seen[d] = true
result = append(result, d)
}
}
if len(result) < len(domains) {
fmt.Printf("dedup: %d -> %d domains\n", len(domains), len(result))
}
return result
}
func read_tmp() string {
file, err := os.Open("README_TEMP.md")
if err != nil {
panic(err)
}
defer file.Close()
content, err := ioutil.ReadAll(file)
return string(content)
}
func write_file(content string) {
err := ioutil.WriteFile("README.md", []byte(content), 0644)
if err != nil {
panic(err)
}
}
func get_ip_list(domain string, dnsServer string) []string {
cmd := exec.Command("nslookup", domain, dnsServer)
output, err := cmd.CombinedOutput()
if err != nil {
fmt.Printf("nslookup failed for %s: %v\n", domain, err)
return nil
}
return parse_nslookup_output(string(output), dnsServer)
}
// ipPingResult 记录 IP 的连通性测试结果
type ipPingResult struct {
ip string
delay time.Duration
ok bool
}
// ping_ip 测试 IP 是否可达,返回延迟时间
func ping_ip(ip string, timeout time.Duration) ipPingResult {
start := time.Now()
conn, err := net.DialTimeout("tcp", ip+":443", timeout)
elapsed := time.Since(start)
if err != nil {
return ipPingResult{ip: ip, ok: false}
}
conn.Close()
return ipPingResult{ip: ip, delay: elapsed, ok: true}
}
// get_fastest_ip 获取域名对应的所有 IP,测试连通性,返回响应最快的一个
func get_fastest_ip(domain string, dnsServer string) string {
ips := get_ip_list(domain, dnsServer)
if len(ips) == 0 {
fmt.Printf("no IP found for %s\n", domain)
return ""
}
// 并发测试所有 IP
results := make(chan ipPingResult, len(ips))
for _, ip := range ips {
go func(addr string) {
results <- ping_ip(addr, 3*time.Second)
}(ip)
}
// 收集结果
var reachable []ipPingResult
for range ips {
r := <-results
if r.ok {
reachable = append(reachable, r)
}
}
if len(reachable) == 0 {
fmt.Printf("all IPs unreachable for %s, using first: %s\n", domain, ips[0])
return ips[0]
}
// 按延迟排序,选最快的
sort.Slice(reachable, func(i, j int) bool {
return reachable[i].delay < reachable[j].delay
})
best := reachable[0]
fmt.Printf("%s -> %s (delay: %v)\n", domain, best.ip, best.delay)
return best.ip
}
func parse_nslookup_output(output string, dnsServer string) []string {
// 不依赖中文关键词,直接从输出中提取所有 IP 地址
// Windows nslookup 输出可能是 GBK 编码,中文匹配会失败
// 策略:提取所有 IP,去掉 DNS 服务器自身地址,剩余即为解析结果
ipRegex := regexp.MustCompile(`(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})`)
matches := ipRegex.FindAllStringSubmatch(output, -1)
var ips []string
for _, m := range matches {
ip := m[1]
// 跳过 DNS 服务器自身地址
if ip == dnsServer {
continue
}
ips = append(ips, ip)
}
return ips
}
总结
这个工具的核心思路是:让每个域名都选到真正最快、可达的 IP。关键设计决策:
- TCP 443 探测 > ICMP ping --- 更真实反映 HTTPS 服务可用性
- 两级并发 --- 域名级 + IP 级,大幅缩短总耗时
- 降级策略 --- 不可达时仍保留首个 IP,不丢失域名
- 外部文件 + 内置默认 --- 灵活配置,开箱即用
欢迎在评论区交流改进建议!