2012/06/05
2022/01/18 更新
2024/01/06 更新
C についての基礎的な知識があることを想定。C と違う点を重点的に解説する。
https://go.dev/dl/
例を PC Linux にとれば go1.17.6.linux-amd64.tar.gz をダウンロードすればよい。これをどこかに置く1。僕の場合には(今後、何回もバージョンアップがあることを想定して)置き場所を
GOLANG=$HOME/src/golang
$GOLANG/go が生成され、ここにパッケージが展開されている。Go のソース・ファイルをコンパイルするのに必要な実行ファイルが $GOLANG/go/bin に置かれている。
環境変数
GOROOT=$GOLANG/go; export GOROOT PATH=$PATH:$GOROOT/bin/go; export PATH
$GOLANG にインストールされていると仮定する。従って $GOLANG/go が既に存在する。その下でgo1.17.6.src.tar.gzcd $GOLANG mv go bootstrap tar -xf go1.17.6.src.tar # produces go GOROOT_BOOTSTRAP=$GOLANG/bootstrap cd go/src all.bashすると次のメッセージを得る:
all.bash Building Go cmd/dist using /home/arisawa/src/golang/bootstrap. (go1.17.6 linux/amd64) Building Go toolchain1 using /home/arisawa/src/golang/bootstrap. Building Go bootstrap cmd/go (go_bootstrap) using Go toolchain1. Building Go toolchain2 using go_bootstrap and Go toolchain1. Building Go toolchain3 using go_bootstrap and Go toolchain2. Building packages and commands for linux/amd64. ##### Testing packages. ok archive/tar 0.060s ok archive/zip 0.142s ... ... ALL TESTS PASSED --- Installed Go for linux/amd64 in /home/arisawa/src/golang/go Installed commands in /home/arisawa/src/golang/go/binこれで終了。
http://p9.nyx.link/golang/ で解説されている。この解説が参考になるかも知れない。
完全な文法は
The Go Programming Language Specification
https://go.dev/ref/spec
に書かれている。しかし、これを初心者が読むのはしんどいだろう。
package main
func fun1(n int){
for k:=0; k < n; k++ {
print(k," ")
}
println()
}
func main(){
println("こんにちは")
var x,y int
x = 20
y = 3
println(x+y,x*y,x/y,x%y)
fun1(10)
}
the result
basg$ go run a.go こんにちは 23 60 6 2 0 1 2 3 4 5 6 7 8 9 -bash$
;" に置き換えられて解釈される。";" は C と同様に命令の区切り子である。func main()
{
と書くとシンタックスエラーになる。なぜなら、func main();{
と解釈されるから。
{ }"{ }" を省けない。
任意
環境変数:
GOROOT: 配布パッケージの場所を知らせる。配布パッケージはどこに置いてもよい。
GOPATH: ユーザー作成(サードパーティを含む) の Go 関係のファイルの場所を知らせる。
Go のプログラム自体はどこに置いても可。しかし $GOPATH/src の下に置くのが管理上望ましい。
go env GOPATH
GOPATH が判る。GOPATH が環境変数として定義されていない場合には$HOME/go
GOPATH であるとされる。
package 宣言は必須main を含む場合には main を指定
io.Args を見よ。return 0 あるいは exit(0) で終わる必要はない。
var x,y int
変数の型の決定は、明示的な宣言によるか、または代入による。
以下の3つのスタイルがある:
(1) var x,y,z int // x=0, y=0, z=0 で初期化
(2) var x,y,z = 2,3,5 // x=2, y=3, z=5 で初期化
(3) x,y,z := 2,3,5 // x=2, y=3, z=5 で初期化
注1: (3) は代入文と言うより、宣言文である。
(2) と (3) の実質的な違いはどこにあるか? → 注3 および 注4 を見よ
そもそも (2) は言語設計として必要なのか? → 注4 を見よ
こんな書き方も許される:
var( a = 2 b = "foo" c = true )関数の中でも、外でも OK
注2: (1) のタイプのように、明示的な型宣言はプログラミングの大きな負担である。僕が Java を使わない最大の理由がこの点にある。インタープリタ言語の多くは、型宣言を要しないので気楽にプログラムができる。他方では可読性を害し、バグの原因になり、バグ取りを困難にする。Go はこの問題を解決しようとしている。
注3: (1) と (2) のタイプは関数の外で使えるが、(3) のタイプは関数の外では使えない。
注4: (3) のタイプは、その変数が利用されていないとコンパイラに文句を言われ、コンパイルできない。他方、(1) や (2) のタイプは、関数の外で宣言された場合には、その変数が利用されていなくてもコンパイラは文句を言わない。(この特徴は package の中で宣言あるいは初期化された多数の変数を利用できるようにするために必要である。)
"abc"+"de"
var a byte; var b int
a + b
int(a)+b
println は改行を伴う。以下に標準出力に吐き出す類似の関数を挙げる。print println fmt.Print fmt.Println fmt.Printf
print() と println() は原始的な出力関数で、正式のプログラムコードでは使わない方が良い。デバッグ用に限定して使える。原始的なるが故に、Go の動作を理解するのに良いかも知れない。
print() は改行を伴わない。また、データとデータの区切りに空白を付加しない。
print() や println() は、ポインタ変数に対してはアドレスを出力する。他方参照しているデータを書き出すには fmt package の関数を使う。
fmt を使う時には import 宣言が必要
package main import ( "fmt" )のように宣言しておいて
fmt.Println("Hello")
fmt." が面倒ならpackage main import ( . "fmt" )とする。すると
Println("Hello")
fmt." を省ける。なぜ原始的な出力関数を大文字で始めなかったかが理解できるだろう。なお Go の名前規則の中に、小文字で始める名前は package の中で private、大文字で始める名前は public というのがある。public な名前は、他の package から名前が参照される。
ここに挙げた出力関数はバッファリングしないので遅い。デバッグ用。
バッファリングが必要なら、"bufio" を使う。
"a string in double quote" `a string in back quote`
追記: Python や Lua などのスクリプト言語に慣れていると、'abc' も文字列であると錯覚する。もちろんコンパイラが跳ねる。
頭の中が????...になる。今日もそうだった。(2025/01/03)
'a' 'あ'
' ' で囲む。この辺は C と同じであるが、許される文字は Unicode に拡張されている。
' ' で括ったつもりでも、1文字と認識しないのでエラーになるだろう。
rune は uint32 の別名
byte は uint8 の別名
C 風の書き方がサポートされている。
for k:=0; k < n; k++ {
print(k," ")
}
しかし次の書き方は NG で要注意。
package main
import(
. "fmt"
)
func f(n,m int) bool{
if n >= m {
return false
}
return true
}
func main(){
ok := true
for k:=0;ok;k++ {
//ok = f(k,3) // OK
ok := f(k,3) // NG
Println(k,ok)
}
}
うまく行かない原因は、for の条件部の ok は for の本体ブロックの中での ok とは異なるからである。ここは ":=" ではなく "=" としなくてはならない。(コンパイラがチェックしてくれるべきだと思う)
for(k=0; k < n; k++) {
......
}
( ) が無い。無くても意味が通じるから。{ } が必要である。( { } の中に命令が1つしか無い場合も):= が使われている。k は for の中だけの一時変数だから。
注2:
while ~ {....} や do {....} until ~ は Go には無い。for だけでやっていく。
詳しくは後に。
package main
// Ssanf
// go doc fmt.Sscanf
import (
. "fmt"
)
func main() {
var x,y int
// func Sscanf(str string, format string, a ...interface{}) (n int, err error)
input := "123 45"
Println(input)
n,err := Sscanf(input, "%d %x",&x, &y)
Println(err,n,x,y)
s := Sprintf("%d %x", x, y)
Println(s)
}
output:
123 45 <nil> 2 123 69 123 45
文字に関する、いくつかの基本型: string, byte, rune
package main
// quote
import "fmt"
func main(){
var s = "こんにちは"
var b = []byte(s)
var t = string(b)
var u = 'あ' // this type is rune
println(s) // こんにちは
println(b) // [15/15]0xf840028220
fmt.Println(b) // [227 129 147 227 130 147 227 129 171 227 129 161 227 129 175]
println(t) // こんにちは
fmt.Printf("%x\n",u) // 3042
}
/*
output is shown at the right side of "//"
3042 is 0x3042, unicode of 「あ」
*/
// これより右がコメント (行末まで) /* この中がコメント */
" " で囲っているのはそのためであろう。var s = "こんにちは" var b = []byte(s)
[]byte
var b = []byte(s)
s を基に slice 型データが作られる。「基に」と言ったのは、b が指し示しているのは s そのものではないからである。(s のコピーである)
この例題で示されているように、string と byte 配列 ( []byte ) は相互に変換できる。
string はもっぱら UTF-8 でエンコードされた文字列のための変数型であるが、任意の byte 列を string に変換できるようである。次の例で、string と []byte ととの違いを示す。
var a string var b []byte a = "Hello" b = []byte(a) fmt.Println(a, a[2:5],b[2:5]) // Hello ell [108 108 111]
s の長さは関数 len(s) で分かる。len("こんにちは") は 5 ではなく、15 を返す。15 は "こんにちは" を UTF-8 でエンコードしたときの byte 数である。
補注: string と言いながら string らしからぬこの規則は変なのである。
package main
import "fmt"
func main(){
var s = "こんにちは"
var b = []byte(s)
var t = string(b)
println(len(s),s) // こんにちは
println(len(b),b) // [15/15]0xf840028220
fmt.Println(b) // [227 129 147 227 130 147 227 129 171 227 129 161 227 129 175]
println(len(t),t) // こんにちは
b[0] = 0
b[1] = 0
b[2] = 0
t = string(b)
println(len(t),t)
println(len(s),s)
}
この実行結果は15 こんにちは 15 [15/15]0xf840028220 [227 129 147 227 130 147 227 129 171 227 129 161 227 129 175] 15 こんにちは 15 んにちは 15 こんにちは
数字を表す文字列から数字を生成するに、
fmt.Sscanf()
strconv.Atoi を使う。package main
import(
. "fmt"
"strconv"
)
func main(){
var s = "123"
var x int
x,err := strconv.Atoi(s)
Println(err,x)
n,err := Sscanf(s, "%d", &x)
Println(n,err,x)
}
output<nil> 123 1 <nil> 123
":=" で定義される。(宣言と初期化を兼ねている)":=" の効果をみるプログラムを示す。package main
func main(){
x:=3 // temporal variable, use ":=" without "var".
println(x) // 3
{
x=x+2
println(x) // 5
x:="abc"
println(x) // abc
}
println(x) // 5
}
コメント "//" の右に println() の出力が示されている。
同じ変数名 x を使っていても、実体は異なる変数であることが分かる。
package main
func main(){
var x int
for x:=0; x <3; x++ {
println(x)
}
println(x) // o
for x=0; x < 3; x++ {
println(x)
}
println(x) // 3
}
for 文のための制御変数のために、わざわざ var で変数宣言するのは嫌だから、":=" を導入したらしい。
しかし、伝統的な C のスタイルだと、繰り返しが最後まで行われたことが、容易に分かるメリットもある。
package main
import "fmt"
func main(){
var primes = [10]bool{}
primes[2]=true
primes[3]=true
primes[5]=true
primes[7]=true
for i:=2; i<10;i++{
fmt.Printf("%d:%-8v",i,primes[i])
}
fmt.Println()
}
譜 array2.go
あいるは
package main
import "fmt"
func main(){
var primes = [10]bool{
2: true,
3: true,
5: true,
7: true,
}
for i:=2; i<10;i++{
fmt.Printf("%d:%-8v",i,primes[i])
}
fmt.Println()
}
[10]bool{}
" で初期値を指定するが、この場合には指定されていないので、初期値は false となる。
Printf の書式の中の "%v" は、変数の型に合わせて適当にやってくれる。"%s" や "%d" の代用になる。
なお Printf の書式の中の "%T" は、変数の型を表す。
-bash$ go run array2.go 2:true 3:true 4:false 5:true 6:false 7:true 8:false 9:false -bash$
次に、配列のサイズを、初期値から決める例を挙げる。
package main
import "fmt"
func main(){
var days = [...]string{"sun","mon","tue","wed","thu", "fri","sat"}
for i:=0; i<len(days);i++{
fmt.Printf("%5s",days[i])
}
fmt.Println()
}
...」は任意個を表す。
for _,v := range days {
fmt.Printf("%5s",v)
}
-bash$ go run array1.go sun mon tue wed thu fri sat -bash$
var days = [...]string{"sun","mon","tue","wed","thu", "fri","sat"}
var days = []string{"sun","mon","tue","wed","thu", "fri","sat"}
package main
import "fmt"
func main(){
var a = [...]string{"sun","mon","tue","wed","thu", "fri","sat"}
var b = a
a[1] = "lua"
for i:=0; i<len(a);i++{
fmt.Printf("%5s",a[i])
}
fmt.Println()
for i:=0; i<len(b);i++{
fmt.Printf("%5s",b[i])
}
fmt.Println()
}
譜 array3.go
このプログラムでは b もまた array であり、a とは独立したデータを保有している。(a のデータがコピーされている。) 従って、a を変更しても b は変更されない。この事は次の結果で分かる。
-bash$ go run array3.go sun lua tue wed thu fri sat sun mon tue wed thu fri sat -bash$
slice 型変数はデータの本体を保有しない。(実際には array へのポインタである。)
次のプログラムでは a も b も共通のデータへのポインタなので、a を変更する(a を通じて本体を変更する)と、
package main
import "fmt"
func main(){
var a = []string{"sun","mon","tue","wed","thu", "fri","sat"}
var b = a
a[1] = "lua"
for i:=0; i<len(a);i++{
fmt.Printf("%5s",a[i])
}
fmt.Println()
for i:=0; i<len(b);i++{
fmt.Printf("%5s",b[i])
}
fmt.Println()
}
譜 array4.go
結果は次のように、b も変更される。
-bash$ go run array4.go sun lua tue wed thu fri sat sun lua tue wed thu fri sat -bash$
package main
import "fmt"
func main(){
a := [4]int{}
a[0] = 2
a[1] = 3
a[2] = 5
a[3] = 7
fmt.Println(a[1:3]) // [3 5]
}
この例では a[1:3] の値は [3 5] である。これは、a[1] と a[2] の値である。a を配列とすると
a[n:m]
a の部分配列(a[n],a[n+1],...,a[m-1])が得られる。n,m は次の関係を満たさなくてはならない:0 <= n <= m < len(a)
package main
import (
. "fmt"
)
func main() {
var a = [3]int{2,3,5}
var b = [2]int{7,11}
// Println(append(a[:],b[:])) //NG
Println(append(a[:],b[:]...)) //OK [2 3 5 7 11]
Println(append(a[:],b[0],b[1])) //OK [2 3 5 7 11]
}
b[:] は b[0:] と同じである。全体を slice として扱いたい場合には、この方が良いであろう。b[:]... は奇っ怪なように感じられるが、これは配列の要素を丸ごと関数の引数として渡したい場合に便利である。またこのテクニックを知らないと困ることがある。例えばデバッグ用の dbgPrintln() を作りたいときなど..
make([]int,5,100)
配列の構成に必要な要素の個数が、実行してみないと分からない場合がある。(そのようなケースはドキュメント処理では極く普通である。)
Python では、そのようなケースは List で切り抜けている。Go はあくまで伝統的な配列を使う。(速度が速い)
配列の大きさが必要に応じて変化するような配列を定義するには make() を使う。次のプログラムで make() の使い方と特性を示す。
package main
func main(){
a := make([]int,5,100)
a[0] = 2
a[1] = 3
a[3] = 5
println("len:",len(a))
a = append(a,7,11,13)
println("len:",len(a))
for i:=0; i<len(a);i++{
println(a[i])
}
}
このプログラムのa := make([]int,5,100)
a のサイズを 5 として、100 まで大きくなる初期値を与えている。実際には、100 を超える事が許されるが、100 を超えるとメモリーの再配置が内部で行われる(そのために時間のロスが発生する)可能性があり、適切な初期値を与えた方が良い。a := make([]int,5)
指定された配列の初期値(ここでは5)を超える部分への追加は 関数 append() で行う。
プログラムの実行結果は次のようになる。
len: 5 len: 8 2 3 0 5 0 7 11 13
初期配列を超える要素(ここでは a[n] (n >4) )の参照は、append() でデータが与えられていない限り実行時のエラーになる。この仕様は安全性のために当然である。
配列の要素には暗黙の初期値(整数の配列の場合には 0)が存在する。つまり明示的に初期化されていない要素が式の中に使われてもエラーにならない。この仕様は危険なのであり、安全性のためには Lua のように、nil を返した方が良い。しかし、Go における配列の概念はCの配列と同じで、要素は型で示されているとおりである。そのために nil は返せない。そもそも整数型の変数には、初期化されたのか否かの印を付ける事ができないのである。
make([]int,0)make([]int,5) の特殊ケースである。
package main
import (
. "fmt"
)
func main() {
var a = make([]int,0)
for k:=0; k <10; k++ {
a = append(a[:],2*k)
}
Println(a)
}
output:
[0 2 4 6 8 10 12 14 16 18]
リストは package によってサポートされている。package は
import "container/list"
http://golang.org/pkg/container/list/
map のキーを整数に限定すると、配列のように使える。
package main
func main(){
var d = map[int]string{}
d[1]="alice"
d[2]="bob"
d[6]="frank"
println(len(d))
for k,v := range d {
println(k,v)
}
}
結果:
3 1 alice 2 bob 6 frank
package main
func main(){
var d = map[int]string{1:"alice",2:"bob",6:"frank"}
println(len(d))
for k,v := range d {
println(k,v)
}
}
v,ok := d["david"]
package main
func main(){
var d = map[string]int{"alice":12,"bob":18,"carol":22}
v,ok := d["alice"]
println(v,ok) // 12, true
v,ok = d["david"]
println(v,ok) // 0, false
}
なお、map に登録されていないキーの要素の値は Zero value (整数の場合には 0, 文字列の場合は "", ポインタの場合には nil) となる。Zero value だけを見て、登録の有無を判断できないのは、この例をみても明らかである。
存在しないキーを使ってもエラーにならない!
package main
func main(){
var d = map[string]int{"alice":12,"bob":18,"carol":22}
n := d["alice"] + d["david"]
println(n) // 12, not error
//n,ok := d["alice"] + d["david"] // error, assignment count
//println(n,ok)
}
ここでは構造体 "struct" の使い方(宣言の方法、値の与え方など)を例題で示す:
// lesson to structured variables
package main
import (
. "fmt"
)
var p [2] float64
type Point struct {
x,y float64
}
var q Point
type Person struct {
name string
age int
}
var a1 = Person {"alice", 16}
type T struct {
name string
age int
etc U
}
type U struct {
mail string
tel string
}
func main() {
Println(p)
p[0] = 2.1
p[1] = 1.2
//p[2] = 3.1 // this should be error
Println(p)
// which is same as:
p = [2] float64 {2.1, 1.2}
Println(p)
Println(q)
q.x = 2.1
q.y = 1.2
Println(q)
// which is same as:
q = Point{2.1, 1.2}
Println(q)
Println(a1)
b := T{"bob",18,U{"bob@gmai.com","0123-456-789"}}
Println(b)
}
output:
[0 0]
[2.1 1.2]
[2.1 1.2]
{0 0}
{2.1 1.2}
{2.1 1.2}
{alice 16}
{bob 18 {bob@gmai.com 0123-456-789}}
最後の行を見て分かる通り、Println(b) は構造体の深部をちゃんと書き出している。
package main
import (
. "fmt"
)
type Foo struct{
int
x,y int
}
func main() {
var a Foo
a.int = 5; a.x = 2; a.y = 3
Println(a)
}
ここに現れた
type Foo struct{
int
x,y int
}
は
int int
x,y int
a.int = 5
次の例は、コマンド引数の処理を示す。
package main
import "os"
func main(){
for n:=0; n < len(os.Args); n++ {
println(n,os.Args[n])
}
}
package main
import "os"
func main(){
for n,v := range os.Args {
println(n,v)
}
}
xxx.go とすると、コンパイルと実行はgo build xxx.go && ./xxx alice bob caro
xxx が生成される。
なお、実行ファイル xxx.go の生成をしないならば、
go run xxx.go alice bob carol
xxx.go の所で Go のソース・ファイルを複数個並べると、違った意味になるので注意。
実行ファイル xxx を $GOPATH/bin の中に生成したければ
go install xxx.go
#include <stdio.h>
int a = f(3); // compile error
int f(int x) {
return 2*x;
}
int main(int argc,char **argv){
printf("OK %d\n",a);
return 0;
}
次のように、関数の中で初期化しなくてはならない。
#include <stdio.h>
int a;
int f(int x) {
return 2*x;
}
int main(int argc,char **argv){
a = f(3);
printf("OK %d\n",a);
return 0;
}
# a=f(3) # error def f(x): return 2*x a=f(3) print "OK",a
Python に限らず、大域変数が実行時に(関数の外で)初期化できることは、スクリプト言語の常識である。
ただし、a=f(3) が実行される時点で f(x) が定義されていなくてはならない。
package main
var a = f(3)
func f(x int) int {
return 2*x
}
func main(){
println("OK",a)
}
この仕組みを使うと、例えば、コマンド引数を基に大域変数を次のように初期化できる。
package main
import (
"os"
"strconv"
)
var b,err = strconv.Atoi(os.Args[1])
var a = f(b)
func f(x int) int {
return 2*x
}
func main(){
println("OK",a)
}
var の部分は次のように書いてもよい。
var ( b,err = strconv.Atoi(os.Args[1]) a = f(b) )
flag package に応用の具体例が見られる。
コマンド引数の情報を、関数 main の引数ではなく、os.Args でプログラムに渡す設計にしたのは、このような事情があるのかも知れない。
package main
import (
"os"
"log"
)
func main(){
var wr = os.Stdout
var bs []byte
bs = make([]byte,4*1024)
for _,f := range os.Args[1:] {
rd,err := os.Open(f)
if err != nil {
log.Fatal(err)
}
for n,err := rd.Read(bs) ; n > 0; {
if err != nil {
log.Fatal(err)
}
wr.Write(bs)
n,err = rd.Read(bs)
}
rd.Close()
}
}
import ( "os"; "log" )
;" ではなく、"," で区切りたくなるが、";" にしたのは、例題のように、一つの package を一つの行に書かせたかったのだろう。もしも、"," で区切るルールにすると、package 名のあとに "," を書かなくてはならなくなる。
var bs []byte bs = make([]byte,4*1024)
var bs = make([]byte,4*1024)
_" が準備されている。変数 "_" はゴミ箱のような変数で、ここに代入された値が使用されなくても、コンパイラは文句を言わない。
関数の外で定義された変数に関しては、それを利用していなくてもコンパイラは文句を言わない。
if err != nil {
nil でない値は true と見なされない)
log.Fatal() はエラーの理由をログファイルに書き出し、os.Exit(1) (異常終了) で終わる関数である。ログファイルが指定されていない場合には、標準エラーに出力する。
この例では for 文のブロックの中に ":=" が使われている。具体的には
rd,err := os.Open(f)
for 文のブロックの中における ":=" の使用はできるだけ避けるべきである。C とは異なり golang では ":=" で定義された変数は、for 文の条件部に現れる変数とは(たとえ同名の変数であっても)関係ない。そのために繰り返しの停止条件に関わる変数を for プロックの中で ":=" で与えると無限ループに陥ることがある。
こり例題では
defer bufio.NewReader() bufio.ReadString() os.WriteString() err.Error() for
package main
import (
"os"
"bufio"
"log"
)
func parse(path string){
var line string
file,err := os.Open(path)
if err != nil {
log.Fatal(err)
}
defer file.Close()
rd := bufio.NewReader(file)
wr := os.Stdout
line,_ = rd.ReadString('\n')
for len(line) > 0 {
// do something here, for example:
wr.WriteString(line)
line,_ = rd.ReadString('\n')
}
}
func main(){
parse(os.Args[1])
}
関数 parse() の中の
defer file.Close()
defer に続く関数が parse() の終了時に実行される事を指定している。defer で指定される関数は「後始末関数」である。エラー処理の後にファイルをクローズするのを忘れがちになるのだが、defer を使えば、そうしたバグから解放される。
defer は複数指定してもよく、後に指定した後始末関数から順に実行される。
bufio" の使用例を示す。ReadString が便利である。この関数は、一行の文字数があらかじめ分かっていない(つまり非常に長い行が含まれている可能性がある)場合に使われる。
ReadString() が返す値はファイルに含まれる 1行の文字列であり、行の区切り文字(このケースでは '\n') を末尾に含む。
UNIX の伝統的なテキストファイルの形式では、ファイルに含まれる最後の文字は '\n' である。しかし、この形式に従わないテキストファイルが存在するので注意が必要である。
bufio.ReadString() は Plan9 の readstr() の Go での実装である。readstr() は C の関数であるために、得られた行は明示的に free() でメモリを解放する必要があった。しかし、bufio.ReadString() の場合は、メモリの解放はガーベッジコレクタが行っているはずである。
ReadLine() があるが、こちらは(メモリ効率は良いが)運用に次の注意が要求される。ReadLine() で破棄される。ReadLine() はあらかじめ一行の最大サイズが分かっていない場合に使うとバグを発生させるか、あるいは面倒な処理が要求される。また得られた行を蓄積して、ファイルを読み終わってから解析するわけには行かない。ReadLine() 相当の関数はサポートされないのであるが、Go は C の細やかさも追求しようとしているのだろう。( Go の ReadString() は Python の readline() に相当する )
for len(line) > 0 {
for 論理式 {
もちろん、この部分は
for ; len(line)>0; {
'\n' で終わらないテキストファイルに対しては、'\n' を追加する。
package main
import (
"os"
"bufio"
"log"
)
func parse(path string){
var line string
file,err := os.Open(path)
if err != nil {
log.Fatal(err)
}
defer file.Close()
rd := bufio.NewReader(file)
wr := os.Stdout
line,err = rd.ReadString('\n')
for err == nil {
// do something here, for example:
wr.WriteString(line)
line,err = rd.ReadString('\n')
}
if len(line) > 0 {
wr.WriteString(line+"\n")
}
}
func main(){
parse(os.Args[1])
}
読み取った行のサイズが正であるにも係わらず、err が発生するのは '\n' で終わっていないからである。このケースでは err == io.EOF となっている。
普通のファイル、つまりファイルに含まれる文字数が判っているファイルでは、その情報を利用してファイルを読み取ることができる。この場合に簡便なのは os.ReadFile() である。次に例を示す。
package main
import(
. "fmt"
"os"
)
var usage = "usage: read1 file"
func Error(s interface{}){
Fprintln(os.Stderr,s)
os.Exit(1)
}
// look src/os/file.go for os.ReadFile()
func main(){
args := os.Args[1:]
if len(args) != 1 {
Error(usage)
}
b,err := os.ReadFile(args[0])
if err != nil{
Error(err)
}
Print(string(b))
}
ファイルの内容を全てメモリーに蓄えるので、大きなファイルには向かない。
読み取った結果をメモリーに蓄えたいのだが、予め必要なメモリーのサイズが判らないことがある。例えば標準入力からの読み取りの場合である。そのような場合には os.ReadFile は役に立たない。しかし io.ReadAll(rd) が使える(次を見よ)。 この例は Hget でも使われている。
なお関数 Error() における interface{} については「型無し変数」を見よ。
package main
import(
. "fmt"
"os"
"io"
"crypto/sha256"
)
var usage = "sha256sum [file]"
func Error(s interface{}){
Fprintln(os.Stderr,s)
os.Exit(1)
}
func main(){
var err error
var bs []byte
args := os.Args[1:]
rd := os.Stdin
switch(len(args)){
case 0:
case 1:
f := args[0]
rd,err = os.Open(f)
if err != nil {
Error(err)
}
default:
Error(usage)
}
bs,err = io.ReadAll(rd)
if err != nil {
Error(err)
}
rd.Close()
sum := sha256.Sum256(bs)
Printf("%x\n",sum)
}
複数のファイルが指定された場合にも対応すべきだが、サボられている。
package main
import "fmt"
type typeless interface{}
func main(){
var x typeless
x = 3
println(x)
println(x.(int)) // 3
fmt.Println(x) // 3
x = "alice"
println(x)
println(x.(string)) // alice
fmt.Println(x) // alice
}
このプログラムでは、変数 x は、整数 3 の代入を許したり、文字列 "alice" の代入を許したりしている。このような柔軟な変数はインターフェース型変数の特殊ケースであり、型名を var x interface{}
注1: このプログラムでは、interface{} を typeless として定義している。これは僕の好みである。
注2: Golang の現在の版(Go 1.18以降)では "typeless" ではなく "any" が初めから定義されて使えるようになっている。単に interfacace{} の別名であれば、わざわざ "any" を導入するほどのことではなかったろうと思える。"any" については僕はまだまた理解が不足している。
var x typeless x = 2 println(x + 3) // error
var x typeless x = 2 println(x.(int) + 3) // OK
Golang では Python (あるいは現代的な言語)のように複数の戻り値を指定できる。これは非常にありがたいのである。2つの方法を関数 f(x,y) と関数 g(x,y) で示す。関数 f(x,y) は普通のスタイルである。他方 g(x,y) は他の言語に慣れているとちょっと奇っ怪であるが、エラーコードを返すのに便利なように設計されている。次にそのサンプルを示す。
package main
import (
. "fmt"
"os"
)
var usage = "usage: a1"
func Error(s interface{}){
Fprintln(os.Stderr,s)
os.Exit(1)
}
func f(x int,y int) (int, int) {
return x+y, x*y
}
func g(x int, y int) (sum int, prod int) {
sum = x+y
prod = x*y
return
}
func main() {
Println(f(2,3)) // output: 5 6
Println(g(2,3)) // output: 5 6
}
/* look
https://go.dev/ref/spec
for details
*/
package main
func f(args ... int){
for k,v := range args{
println(k,v)
}
}
func main(){
f(2,3,5)
}
結果は0 2 1 3 2 5宣言
args ... int
args が int 型の可変個数引数である事を示している。
この例では可変個数部分の全てが単一タイプ( int )であるが、型の部分も含めて自由にデータを関数に渡したい場合にはどうするか?
次のプログラムがヒントになる。
package main
import "fmt"
func f(args ... interface{}){
for k,v := range args{
println(k,v)
fmt.Println(k,v)
}
}
func main(){
f(2,3,"alice")
}
この出力は0 (0x56820,0xf800000002) 0 2 1 (0x56820,0xf800000003) 1 3 2 (0x56a60,0xf840028220) 2 alice
package main
import(
. "fmt"
"errors"
)
// $GOROOT/src/errors/errors.go
func main(){
err := errors.New("blah blah")
Println(err)
}
ポインタ(pointer)は C と同様にメモリーのアドレスである。ポインタにおける "*" と "&" の使い方は C の方法を踏襲している。
他の言語に比べて C ではポインタを使えることが強力な武器になったが、他方では(コーディングミスによる)危険性を孕んでいた。この問題は、配列へのポインタとして slice 型を導入することによって回避している。
slice 変数とは(与えられた)配列の範囲を表す情報を含む変数で (ポインタ、長さ、容量) の3つから構成されている1。例えば a を長さ7の配列とすると a[2:5] で a[2],a[3],a[4] から構成される a の部分配列が得られるが、このときのポインタは a[2] を指しており、長さは 3 、容量は 7 である。容量は許容されるインデックスの範囲を知るのに利用される。
Slice 変数は関数に部分配列を渡すのに使われることが多い。ポインタ変数も関数引数に使われることもあるが、使い方の例を次に示す。
package main
import (
. "fmt"
"unsafe"
)
func f(a []int){
Println(a)
}
func g(a *[]int){
Println(*a)
}
func main() {
var n int
var a [7]int
var b []int
n = 1
for k:=0; k<7; k++ {
n *= 2
a[k] = n
}
b = a[2:5]
Println(unsafe.Sizeof(b)) // output: 24
Println(b) // output: [8 16 32]
f(b) // output: [8 16 32]
g(&b) // output: [8 16 32]
// g(&a[2:5]) // NG
}
この output は
24 [8 16 32] [8 16 32] [8 16 32]である。
unsafe.Sizeof(b) はデバッグ用の特殊な関数で b のサイズを知るのに使われる。もちろん CPU に依存する。unsafe にはこの他 CPU 依存性を持ったいろいろなデバッグ用の関数が存在する。
f(b) は f(a[2:5]) と置き換えても構わないが、g(&a[2:5]) は許されない。slice 変数は十分に軽量であるから、敢えてポインタで渡す必要はないのである。
look alse https://go.dev/ref/spec
copy が準備されている。
slice 同士でコピーを行う。
package main
import (
. "fmt"
)
func main(){
var a = [5]int{2,3,5,7,11}
var b []int
var c [5]int
var n int
c = a
Println(a,c) // [2 3 5 7 11] [2 3 5 7 11]
n = copy(b,a[1:4])
Println(n,a,b) // 0 [2 3 5 7 11] []
n = copy(c[3:],a[1:4])
Println(n,a,c) // 2 [2 3 5 7 11] [2 3 5 3 5]
n = copy(a[3:],a[1:4])
Println(n,a) // 2 [2 3 5 3 5]
}
正規表現
2022/01/14
2022/02/11 追加
Go の正規表現は Perl, Python などの unix でポピュラーなスクリプ言語のシンタックスを受け継いでいるが、これらの「正規表現」には問題があるとのことで[2]、全てがサポートされてはいない。詳しくは
go doc regexp/syntax
を実行するか、または文献[1]を見よ。さらに
$GOROOT/src/regexp/regexp.go
のコメントにも詳しい解説がある。
プログラムサンプル
次に示すのは Go で書いた grep である。ただし flag や option はサポートされていない。
unix の grep は貧弱であり egrep の方が使いやすい。Go の正規表現は egrep の正規表現に近い。
package main
import (
. "fmt"
. "regexp"
"os"
bio "bufio"
)
var usage = "usage: grep expression [file ...]"
func Error(s interface{}){
Fprintln(os.Stderr,s)
os.Exit(1)
}
func grep(re *Regexp, fd *os.File){
var m bool
rd := bio.NewReader(fd)
line,err := rd.ReadString('\n')
for err == nil {
m = re.Match([]byte(line))
if m {
Print(line)
}
line,err = rd.ReadString('\n')
}
if len(line) > 0 { // input doesn't end with '\n'
Println(line)
}
}
func main(){
args := os.Args[1:]
if len(args) == 0 {
Error(usage)
}
expr := args[0]
re, err := Compile(expr)
if err != nil {
Error(err)
}
// Printf("re: %T\n",re) // *regexp.Regexp
args = args[1:]
if len(args) == 0 {
// Printf("Stdin: %T\n",os.Stdin) // *os.File
grep(re, os.Stdin)
os.Exit(0)
}
var fd *os.File
n := len(args)
for k:=0; k < n; k++ {
fd, err = os.Open(args[k])
grep(re, fd)
fd.Close()
}
}
譜 grep.go
ここに現れた
rd.ReadString('\n')
や
re.Match([]byte(line))
は、「対象」 (rd や re) に対する作用を表しているシンタックスで、Python ではよく使われる。しかし Python ではいわば便法で、「対象」の部分は関数引数に移せるが、golang は違う。例えば
Match(expr, []byte(line))
とすると違う意味になる。この場合 "expr" は string で、これを基に Compile が行われ、結果が re.Match([]byte(line)) に渡される。1回きりの場合にはそれでも良いが、この例のように何回も実行される場合には、効率が悪くなる。
これらの使い方は、Go のソースコードを直接調べなくても
hebe% go doc regexp.Match
package regexp // import "regexp"
func Match(pattern string, b []byte) (matched bool, err error)
Match reports whether the byte slice b contains any match of the regular
expression pattern. More complicated queries need to use Compile and the
full Regexp interface.
hebe% go doc bufio.ReadString
package bufio // import "bufio"
func (b *Reader) ReadString(delim byte) (string, error)
ReadString reads until the first occurrence of delim in the input, returning
a string containing the data up to and including the delimiter. If
ReadString encounters an error before finding a delimiter, it returns the
data read before the error and the error itself (often io.EOF). ReadString
returns err != nil if and only if the returned data does not end in delim.
For simple uses, a Scanner may be more convenient.
hebe%
でも判るはずであるが1、なぜか Match については完全ではない。実は Match は regexp.go の中に2つある:
func (re *Regexp) Match(b []byte) bool
func Match(pattern string, b []byte) (matched bool, err error)
しかしこのうちの1つしか表示していないのである2。バグでしょうね3。
このプログラムを書いていて気がついたことを記す。
import の中で
bio "bufio"
となっていのは、
. "bufio"
とすると、「"bufio" はもう "fmt" で import されている」と文句を言われたからである。そこで "." ではなく "bio" で切り抜けた次第である。この部分は少し不自然なので、もっと良い方法があるかも知れない。
プログラムの中で
// Printf(....)
のようにコメントアウトされている箇所が2箇所あるが、関数 grep を作る際に、変数の型を調べる必要があったからである。
なお関数 grep を
func grep(re *Regexp, fd *File){
var m bool
rd := bio.NewReader(fd)
line,err := rd.ReadString('\n')
for err == nil {
m = re.Match([]byte(line))
if m {
Print(line)
}
line,err = rd.ReadString('\n')
}
}
とした場合には、改行コードを含まない「行」が grep で引っかからない。例えば
echo -n alice | ./grep ali
を試してみたらよい。
昔のようにテキストファイルが必ず改行コードで終わる時代は過ぎ去った。現在主に使われているテキストエディタは行エディタではなく文字エディタである。そしてコンピュータの利用者の裾野が広がった結果、改行コードで終わらないテキストファイルが氾濫している。そのようなテキストファイルに対しても正しく動くプログラムが求められる時代になっている。
注1: "hebe%" はプロンプトである
注2: golang のソースコードの置き場所は
go env GOROOT
で判る。環境変数 "$GOROOT" がここに設定されているはずである。(設定されていなければ設定すべし)
ここで取り上げた regexp.go は
$GOROOT/src/regexp/
に置かれている。従って
egrep '^func.* Match\(' $GOROOT/src/regexp/*.go
とすれば見落とすことが無かったであろう。
注3: 開発者に質問したらバクではないという。それ以上の説明はなかった。
参考文献
[1] Syntax
https://github.com/google/re2/wiki/Syntax
[2] Russ Cox: Regular Expression Matching Can Be Simple And Fast
https://swtch.com/~rsc/regexp/regexp1.html
Net
2022/01/17
Dial
net package にある net.Dial を使った簡単なプログラム例を示す。
package main
import (
. "fmt"
"os"
"net"
bio "bufio"
)
var args []string
var usage = "usage: dial addr:port (addr is sysname or IP address)"
func Error(s interface{}){
Fprintln(os.Stderr,s)
os.Exit(1)
}
func main() {
args = os.Args
if len(args) != 2 {
Error(usage)
}
conn, err := net.Dial("tcp", args[1])
if err != nil{
Error(err)
}
rd := bio.NewReader(conn);
b, err := rd.ReadString('\n')
for ; err == nil; {
Print(string(b))
b, err = rd.ReadString('\n')
}
if len(b) > 0 {
Println(string(b))
}
}
譜 dial.go
このプログラムは相手サーバーに tcp で接続して、相手の応答を返す。例えば onamae.com は接続したクライアントの IP アドレスを教えてくれる:
dial ddnsclient.onamae.com:65000
に対する応答は(例えば)
IPV4: 123.48.102.187
.
のようなものである。これは我が家のホーム GW の WAN 側の IP アドレスである。WAN 側の IP アドレスは、ホーム GW に直接アクセスしても判るが、面倒である。外部のサーバーから教えてもらった方が速い。ネット上にはこのようなサービスを行っているサーバーがいくつか存在する。僕がこのようなものを必要としているのは、我が家は固定 IP ではなく、時々 IP が変化する(料金の安い)動的 IP でプロバイダーと契約しているからである。
我が家のサーバー hebe も同様なサービスを、家庭内のクライアントに対して行っている。例えば
meg% dial hebe:8007
192.168.0.6!8007
192.168.0.10!47686
meg% dial hebe:8008
1642213304
meg%
のように、port 8007 に対してはサーバーとクライアントの IP と port を、port 8008 に対しては現在時刻(unix time)を知らせる。サーバー hebe の現在時刻はインターネットの時刻サービを参照しているので、実用上十分な精度を持っているはずである。家庭内の他のサーバーはこれを参照することになっている。
譜の dial.go はいくつかの問題がある。
(a) IPv4 と IPv6 の選択ができない
(b) 柔軟性を欠いている
Listen
次に listener のプロトタイプを示す。指定されたポート 8000 にアクセスすると、"Hello World" を表示するだけの、実用的価値のないサーバーである。
package main
import (
. "fmt"
"os"
"net"
"io"
)
var usage = "usage: srv1"
func Error(s interface{}){
Fprintln(os.Stderr,s)
os.Exit(1)
}
//https://pkg.go.dev/net#example-Listener
func handleConn(conn net.Conn){
_, err:= io.WriteString(conn,"Hello World\n")
if err != nil {
Fprint(os.Stderr,"#Error ",err)
}
conn.Close()
}
func main() {
ln, err := net.Listen("tcp", ":8000")
if err != nil {
Error(err)
}
for {
conn, err := ln.Accept()
if err != nil {
Error(err)
}
//Fprintf(os.Stderr,"%T\n",conn) // *net.TCPConn
go handleConn(conn)
}
}
譜 srv1.go
ln.Accept() の所で for ループになっているのは、クライアントから何回でもアクセスされるからである。
解せない箇所がある。ln.Accept() が返す conn の型を調べると *net.TCPConn となっている。ところが
func handleConn(conn *net.TCPConn){
と書くと、コンパイラが文句を言う。ここは conn net.Conn でなくてはならないと。
srv1 は次のように実行される。
meg% go build srv1.go
meg% srv1&
meg% dial meg:8000
Hello World
meg%
今の場合には handleConn の負荷が小さい。そして dial は意地悪をしていない。このケースでは
go handleConn(conn)
の代わりに単に
handleConn(conn)
としても問題はない。しかし一般的に言えば handleConn が直ちに終了する保証はない。go を付加したのは handleConn(conn) を接続ごとに並列的に動作させたいからである。内部では C のfork() 関数あるいは thread が使われているはずである。これを Goroutine と言う。
今度は srv2.go として、アクセスしたときに、ホスト側のアドレスとポート、およびクライアント側のアドレスとポートを教えてくれるサーバーを作成する。srv1.go からの変更点はただ一つ
_, err:= io.WriteString(conn,"Hello World\n")
を
_, err:= io.WriteString(conn,Sprint(conn.LocalAddr(),"\n",
conn.RemoteAddr(),"\n"))
置き換えることだけである。
僕のように C に慣れきっている人間は、ここで手が止まってしまう。Sprint が生成したメモリーはどうなるのだ? このメモリーを開放できないではないか? と。C 言語であればアウトである。しかし Go はゴミ取りをやってくれる。ゴミ取りが正しく働いていれば WriteString が終了した段階でメモリーを開放してくれるはずである。Go を信頼しましょう。
最後に srv3.go として、現在の unix time を教えてくれるサーバープログラムを作成する。srv1.go との変更点は次の2点:
_, err:= io.WriteString(conn,"Hello World\n")
の代わりに
_, err:= io.WriteString(conn,Sprint(time.Now().Unix()))
とすることと、import の中に "time" を含めること
だけである。
補注: srv2.go のプログラム、なぜか Plan9 では動作が変である。Go のバグの可能性が高い。Linux では問題ない。なお Plan9 で同じことを行うには、数行のシェルスクリプトで足りる。ファイル t1 を
#!/bin/rc
cat $net/local
cat $net/remote
として
aux/listen1 'tcp!*!8000' t1 &
を実行すればよい。
文献
[1] Documentation
https://pkg.go.dev/net
HTTP
2022/01/07
Hget
package main
import (
. "fmt"
"os"
"io"
"net/http"
)
var usage = "usage: hget1 url"
func Error(s interface{}){
Fprintln(os.Stderr,s)
os.Exit(1)
}
func main() {
args := os.Args
if len(args) != 2 {
Error(usage)
}
resp,err := http.Get(args[1])
if err != nil {
Error(err)
}
defer resp.Body.Close()
body,err := io.ReadAll(resp.Body)
if err != nil {
Error(err)
}
Print(string(body))
}
譜 hget1.go
詳しくは文献[1]を見よ。
- 名称を
hget1 にしたのは Plan9 には既に hget があるから
- https の接続もサポートされている。
- HTTP2 は? Yes (注1 を見よ)
- IPv4/IPv6 フラグがない
hget よりも丁寧なエラーメッセージを出している
注1: https://pkg.go.dev/net/http からの引用:
Starting with Go 1.6, the http package has transparent support for the HTTP/2 protocol when using HTTPS.
Web server
次に基礎的な Web サーバーのコードを示す。
package main
import (
. "fmt"
"os"
"net/http"
)
var usage = "usage: hsrv1 dir [port]"
func Error(s interface{}){
Fprintln(os.Stderr,s)
os.Exit(1)
}
/*
Ref:
$GOROOT/src/net/http/example_test.go:60
for this code
*/
func main() {
var port = "80"
// Simple static webserver:
args := os.Args[1:]
if len(args) == 0 || len(args) > 2 {
Error(usage)
}
dir := args[0]
if len(args) == 2 {
port = args[1]
}
err := http.ListenAndServe(":"+port, http.FileServer(http.Dir(dir)))
if err != nil {
Fprintln(os.Stderr, err);
}
}
譜 hsrv1.go
文献[1]と[2]を参考にしたが、いくつか修正されている。修正点は
- document root と port を実行時に指定できるようにしたこと
- 起動時のエラーが判るようにしたこと
CGI を使わないなら、これで十分実用的な personal Web サーバーになっている。
文献[2]では
log.Fatal(http.ListenAndServe(":8080", http.FileServer(http.Dir("/usr/share/doc"))))
になっていたが、この中の port と "/usr/share/doc" を起動時に引数として与えるようにしたのと、log.Fatal は止めて、直接 err の内容を見れるようにしただけである。
log.Fatal を使いたいなら
if err != nil {
Fprintln(os.Stderr, err);
log.Fatal(err)
}
とすれば良い。
Go の開発者たちはセキュリティに敏感である。指定された document root からアクセスがはみ出さないように細心の注意が払われていると想像する。(彼らの開発した Plan9 の Web サーバーのコードを見ると、そうであった)
このサーバーでは
- アクセス先が directory の場合には "
/index.html" を追加する
- "
." で始まるファイルは隠しファイルである。
となっている。
問題点:
- アクセスログをサポートすべきである
CGI のサポート
CGI をサポートしたい場合には次のようになる:
package main
import (
. "fmt"
"os"
"io"
"net/http"
"html"
bio "bufio"
)
type typeless interface{}
var usage = "usage: hsrv2 dir [port]"
func Error(s interface{}){
Fprintln(os.Stderr,s)
os.Exit(1)
}
func fooHandler(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "こんにちは!\n")
}
func barHandler(w http.ResponseWriter, r *http.Request) {
Fprintf(w, "Hello, %q\n", html.EscapeString(r.URL.Path))
}
var dir = ""
/*
look
https://pkg.go.dev/net/http
$GOROOT/src/net/http/example_handle_test.go
$GOROOT/src/net/http/example_test.go:68
for http.Handle and http.HandleFunc
*/
func main() {
var port = "80"
args := os.Args[1:]
if len(args) == 0 || len(args) > 2 {
Error(usage)
}
dir = args[0] // document root
if len(args) == 2 {
port = args[1]
}
if dir[0] != '/' {
Error(`need dir to start with "/"`)
}
if len(dir) == 1 {
Error(`dir must not be "/"`)
}
http.HandleFunc("/foo", fooHandler)
http.HandleFunc("/bar", barHandler)
http.Handle("/", http.FileServer(http.Dir(dir)))
err := http.ListenAndServe(":"+port, nil)
if err != nil {
Fprintln(os.Stderr, err);
}
}
譜 hsrv2.go
Handler は用途に応じて各自コードを修正すること。
文献
[1] Documentation
https://pkg.go.dev/net/http
[2] $GOROOT/src/net/http/example_test.go
HTTPS
2022/01/18
https サーバー
package main
import (
. "fmt"
"flag"
"os"
"io"
"net/http"
"html"
)
type typeless interface{}
var usage = "usage: hsrv3 -c cert.pem -k key.pem dir [port]"
func Error(s interface{}){
Fprintln(os.Stderr,s)
os.Exit(1)
}
func fooHandler(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "こんにちは!\n")
}
func barHandler(w http.ResponseWriter, r *http.Request) {
Fprintf(w, "Hello, %q\n", html.EscapeString(r.URL.Path))
}
var dir = ""
func main() {
var port = "443"
cert := flag.String("c","","cert.pem")
key := flag.String("k","","key.pem")
flag.Parse()
if *cert == "" || *key == "" {
Error(usage)
}
args := flag.Args()
if len(args) == 0 || len(args) > 2 {
Error(usage)
}
dir = args[0] // document root
if len(args) == 2 {
port = args[1]
}
if dir[0] != '/' {
Error(`need dir to start with "/"`)
}
if len(dir) == 1 {
Error(`dir must not be "/"`)
}
http.HandleFunc("/foo", fooHandler)
http.HandleFunc("/bar", barHandler)
http.Handle("/", http.FileServer(http.Dir(dir)))
err := http.ListenAndServeTLS(":"+port, *cert, *key, nil)
if err != nil {
Fprintln(os.Stderr, err);
}
}
/*
example
in one window
hsrv3 -c cert.pem -k key.pem $home/www/doc 8443
in another window
hget2 -f https://meg.local:8443
*/
譜 hsrv3.go
実行例はコードの中にコメントとして与えられている。meg.local は実験用のサーバーである。cert.pem と key.pem の作り方は次の Certificate に示す。
Certificate
自己証明書は次のように作成できる。
gcert=$GOROOT/src/crypto/tls/generate_cert.go
go run $gcert -host meg.local
譜 gencert
ここに meg.local はサーバーのドメイン名で、各自の環境に応じて設定する。
何回も実行する必要がないので、go run で充分だろう。
Ref:
https://go.dev/src/crypto/tls/generate_cert.go
Hget for https
今度の hget は、相手が自己証明の場合に対応している。force flag "-f" がそれである。このフラグを与えてない場合には hget1.go と同じ動作になるはずである。
package main
import (
. "fmt"
"os"
"io"
"net/http"
"crypto/tls"
)
var usage = "usage: hget2 [-f] url"
func Error(s interface{}){
Fprintln(os.Stderr,s)
os.Exit(1)
}
func main() {
var force = false
args := os.Args[1:]
if args[0] == "-f" {
force = true
args = args[1:]
}
if len(args) != 1 {
Error(usage)
}
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: force},
}
client := &http.Client{Transport: tr}
resp, err := client.Get(args[0])
if err != nil {
Error(err)
}
defer resp.Body.Close()
body,err := io.ReadAll(resp.Body)
if err != nil {
Error(err)
}
Print(string(body))
}
譜 hget2.go
通信のトランスポート層に対する制御は
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: force},
}
のように行っている。
次は hget2.go を作成するにあたって、僕が調べたことのメモ。
for http.Client look
https://github.com/golang/go/blob/master/src/net/http/transport.go
we have:
type Transport struct {
...
TLSClientConfig *tls.Config
...
}
and in https://pkg.go.dev/crypto/tls, we have:
type Config struct {
...
InsecureSkipVerify bool
...
}
コードを書くにあたって、記事[2]が参考になった。
https://stackoverflow.com/questions/12122159/how-to-do-a-https-request-with-bad-certificate
基になっている文献は[1]にある。
文献
[1] Documentation
https://pkg.go.dev/net/http#Client.Get
[2] How to get x509 Certificate from http Client in Go
https://stackoverflow.com/questions/12122159/how-to-do-a-https-request-with-bad-certificate